I have a datastore with a submodule, both having one variable:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
namespaced: true,
state: {
foo: true,
},
modules: {
secondmodule: {
namespaced: true,
state: {
bar: 0,
},
mutations: {
updateBar: (state, value) => {
state.bar = value;
}
}
}
}
})
In my app, I want to display a snackbar for 2 seconds when the value of foo changes to true:
<template>
<v-app id="example-1">
<router-link :to="{ name: 'secondpage'}">Link</router-link>
{{data.connected}}
{{this.$dataStore.state.foo}}
<main>
<v-fade-transition mode="out-in">
<router-view></router-view>
</v-fade-transition>
</main>
<v-snackbar success top :timeout="2000" v-model="data.connected">
connection established!
</v-snackbar>
</v-app>
</template>
<script type="text/babel">
export default {
computed:{
data() {
return {
connected: this.$dataStore.state.foo,
bar: this.$dataStore.state.secondmodule.bar
}
}
},
};
</script>
Since foo is initialized with true, the snackbar is shown on startup-
Strangely, when I click the link to secondpage, which mutates the bar variable:
created() {
this.$dataStore.commit('secondmodule/updateBar', 1)
}
the snackbar shows up again and I observed that connected quickly changed to false and true again after clicking the link. The value foo is always true and has never changed, so how come the computed property connected changes?
Edit: Since I failed to provide a working fiddle, I added the code here, if anyone would like to take a look at it
https://github.com/netik25/vueProblem
Related
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.
I have 2 components:
header.vue
nav.vue
In header i have button, when i click, in nav change class. But i cant get parent component :( How to do this?
index.pug:
.g#global
frameheader
.frame
navigation(:navhidden="hiddenNav", :mobileaside="asideMobile", :links=navJson)
.frame__content
block content
main.js:
import navigation from '../vue/components/nav.vue';
import frameheader from '../vue/components/frameheader.vue';
Vue.component("navigation", navigation);
Vue.component("frameheader", frameheader);
Vue.use(VueCookie);
var global = new Vue({
el: "#global",
data() {
return {
hiddenNav: false,
asideMobile: false
}
}
})
Header.vue:
In header i have two buttons, who need to change data hiddenNav and asideMobile in main.js
<template lang="pug">
header.header
.header__left(:class="{'hidden-aside': this.$root.$emit(hiddenNav)}")
a.header__logo(href="")
img(src="img/logo_smart.png", alt="")
button.header__nav(#click="hiddenNav = !hiddenNav")
span
button.header__nav.header__nav_mobile(#click="asideMobile = !asideMobile" :class="{'active': asideMobile}")
span
</template>
<script>
import VueSlideUpDown from 'vue-slide-up-down'
export default {
name: 'frameheader',
data() {
return {
active: null,
val: false
}
},
methods: {
changeMenuType() {
this.$root.$emit(hiddenNav, true);
}
}
}
</script>
Nav.vue:
In .frame__aside i try to read parrent data drom main.js, but its not work (
<template lang="pug">
.frame__aside( :class="{'hidden-aside': navhidden, 'active': mobileaside }")
</template>
<script>
import VueSlideUpDown from 'vue-slide-up-down'
export default {
name: 'navigation',
data() {
return {
active: null,
val: false
}
},
props: {
navhidden: {
type: Boolean,
default: false
},
mobileaside: {
type: Boolean,
default: false
}
}
}
</script>
The way to accomplish what you want is to take advantage of Vue's custom events.
In your #global template, we need to add listeners for toggle-hidden-nav and toggle-mobile-aside:
#global.g
frameheader(#toggle-hidden-nav='hiddenNav = !hiddenNav', #toggle-aside-mobile='asideMobile = !asideMobile', :hidden-nav='hiddenNav', :mobile-aside='asideMobile')
.frame
navigation(:nav-hidden='hiddenNav', :mobile-aside='asideMobile')
.frame__content
NOTE: I've also updated props and events to use kebab case per the Vue docs (see here and here).
In your header component, we need to
$emit the custom events when the buttons are clicked
Pass hiddenNav and asideMobile as props (for :class binding)
<template lang="pug">
header.header
.header__left(:class="{'hidden-aside': hiddenNav}")
a.header__logo(href='')
img(src='img/logo_smart.png', alt='')
button.header__nav(#click="$emit('toggle-hidden-nav')")
span
button.header__nav.header__nav_mobile(#click="$emit('toggle-aside-moble')", :class="{'active': mobileAside}")
span
</template>
<script>
export default {
...
props: {
hiddenNav: {
type: Boolean,
default: false
},
mobileAside: {
type: Boolean,
default: false
}
},
...
}
</script>
Finally, I'd fix your the class bindings in the nav component as well:
<template lang="pug">
.frame__aside( :class="{'hidden-aside': navHidden, 'active': mobileAside }")
</template>
I am new to Vue and have been very confused on how to approach my design. I want my component FileCreator to take optionally take the prop fileId. If it's not given a new resource will be created in the backend and the fileId will be given back. So FileCreator acts as both an editor for a new file and a creator for a new file.
App.vue
<template>
<div id="app">
<FileCreator/>
</div>
</template>
<script>
import FileCreator from './components/FileCreator.vue'
export default {
name: 'app',
components: {
FileCreator
}
}
</script>
FileCreator.vue
<template>
<div>
<FileUploader :uploadUrl="uploadUrl"/>
</div>
</template>
<script>
import FileUploader from './FileUploader.vue'
export default {
name: 'FileCreator',
components: {
FileUploader
},
props: {
fileId: Number,
},
data() {
return {
uploadUrl: null
}
},
created(){
if (!this.fileId) {
this.fileId = 5 // GETTING WARNING HERE
}
this.uploadUrl = 'http://localhost:8080/files/' + this.fileId
}
}
</script>
FileUploader.vue
<template>
<div>
<p>URL: {{ uploadUrl }}</p>
</div>
</template>
<script>
export default {
name: 'FileUploader',
props: {
uploadUrl: {type: String, required: true}
},
mounted(){
alert('Upload URL: ' + this.uploadUrl)
}
}
</script>
All this works fine but I get the warning below
Avoid mutating a prop directly since the value will be overwritten
whenever the parent component re-renders. Instead, use a data or
computed property based on the prop's value. Prop being mutated:
"fileId"
What is the proper way to do this? I guess in my situation I want the prop to be given at initialization but later be changed if needed.
OK, so short answer is that the easiest is to have the prop and data name different and pass the prop to the data like below.
export default {
name: 'FileCreator',
components: {
FileUploader
},
props: {
fileId: Number,
},
data() {
return {
fileId_: this.fileId, // HERE WE COPY prop -> data
uploadUrl: null,
}
},
created(){
if (!this.fileId_){
this.fileId_ = 45
}
this.uploadUrl = 'http://localhost:8080/files/' + this.fileId_
}
}
Unfortunately we can't use underscore as prefix for a variable name so we use it as suffix.
I have a problem where a component within a router-view that is being kept alive does not call its activated lifecycle hook when first created. The created and mounted lifecycle hooks are being called. On a second visit, the activated hook is being called.
The scenario is quite complicated as there is a bit of nesting and slot using involved.
I've tried to create a minimal example which you can find below, or a bit more detailed on https://codesandbox.io/s/251k1pq9n.
Unfortunately, it is quite large and still not as complicated as the real code which I unfortunately cannot share.
Worse, I failed to reproduce the actual problem in my minimal example. Here, the created, mounted, and activated lifecycle hooks are all called when first visiting SlotExample.
In my real code, only the created and mounted, lifecycle hooks are called on the first visit, the activated hook is called on subsequent visits. Interestingly, all lifecycle hooks are called as expected for SlotParent.
The real code involves more nesting and makes use of slots to use layout components.
My code is using Vue 2.5.16 and Vue-Router 3.0.1 but it also doesn't work as expected in Due 2.6.7 and Vue-Router 3.0.2. I am also using Vuetify and Vue-Head but don't think think this has anything to do with my problem.
index.js.
Does anyone have an idea what I could have been doing wrong. I actually suspect a bug in vue-router
when using multiple nested slots and keep-alive but cannot reproduce.
index.js
import Vue from "vue";
import VueRouter from "vue-router";
import App from "./App.vue";
import Start from "./Start.vue";
import SlotExample from "./SlotExample.vue";
const routes = [
{
path: "/start",
component: Start
},
{
path: "/slotExample/:id",
component: SlotExample,
props: true
}
];
const router = new VueRouter({ routes });
Vue.use(VueRouter);
new Vue({
render: h => h(App),
router,
components: { App }
}).$mount("#app");
App.vue
<template>
<div id="app">
<div>
<keep-alive><router-view/></keep-alive>
</div>
</div>
</template>
SlotExample.vue
<template>
<div>
<h1>Slot Example</h1>
<router-link to="/start"><a>start</a></router-link>
<router-link to="/slotExample/123">
<a>slotExample 123</a>
</router-link>
<slot-parent :id="id">
<slot-child
slot-scope="user"
:firstName="user.firstName"
:lastName="user.lastName"/>
</slot-parent>
</div>
</template>
<script>
import SlotParent from "./SlotParent.vue";
import SlotChild from "./SlotChild.vue";
export default {
name: "slotExample",
components: { SlotParent, SlotChild },
props: {
id: {
type: String,
required: true
}
}
};
</script>
SlotParent.vue
<template>
<div>
<div slot="header"><h1>SlotParent</h1></div>
<div slot="content-area">
<slot :firstName="firstName" :lastName="lastName" />
</div>
</div>
</template>
<script>
export default {
name: "slotParent",
props: {
id: {
type: String,
required: true
}
},
computed: {
firstName() {
if (this.id === "123") {
return "John";
} else {
return "Jane";
}
},
lastName() {
return "Doe";
}
}
};
</script>
SlotChild.vue
<template>
<div>
<h2>SlotChild</h2>
<p>{{ firstName }} {{ lastName }}</p>
</div>
</template>
<script>
export default {
name: "slotChild",
props: {
firstName: {
type: String,
required: true
},
lastName: {
type: String,
required: true
}
},
created() {
console.log("slotChild created");
},
mounted() {
console.log("slotChild mounted");
},
activated() {
console.log("slotChild activated");
}
};
</script>
I think you need to put SlotChild within keep-alive block.
Take a look at vue js doc about activated hook
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});
}
},