I have 2 components OperatorsList and OperatorButton.
The OperatorsList contains of course my buttons and I simply want, when I click one button, to update some data :
I emit select with the operator.id
This event is captured by OperatorList component, who calls setSelectedOperator in the store
First problem here, in Vue tools, I can see the store updated in real time on Vuex tab, but on the Components tab, the operator computed object is not updated until I click antoher node in the tree : I don't know if it's a display issue in Vue tools or a real data update issue.
However, when it's done, I have another computed property on Vue root element called selectedOperator that should return... the selected operator : its value stays always null, I can't figure out why.
Finally, on the button, I have a v-bind:class that should update when the operator.selected property is true : it never does, even though I can see the property set to true.
I just start using Vue, I'm pretty sure I do something wrong, but what ?
I got the same problems before I used Vuex, using props.
Here is my OperatorList code :
<template>
<div>
<div class="conthdr">Operator</div>
<div>
<operator-button v-for="operator in operators" :op="operator.id"
:key="operator.id" #select="selectOp"></operator-button>
</div>
</div>
</template>
<script>
import OperatorButton from './OperatorButton';
export default {
name: 'operators-list',
components : {
'operator-button': OperatorButton
},
computed : {
operators() { return this.$store.getters.operators },
selected() {
this.operators.forEach(op =>{
if (op.selected) return op;
});
return null;
},
},
methods : {
selectOp(arg) {
this.$store.commit('setSelectedOperator', arg);
}
},
}
</script>
OperatorButton code is
<template>
<span>
<button type="button" v-bind:class="{ sel: operator.selected }"
#click="$emit('select', {'id':operator.id})">
{{ operateur.name }}
</button>
</span>
</template>
<script>
export default {
name: 'operator-button',
props : ['op'],
computed : {
operator() {
return this.$store.getters.operateurById(this.op);
}
},
}
</script>
<style scoped>
.sel{
background-color : yellow;
}
</style>
and finally my app.js look like that :
window.Vue = require('vue');
import Vuex from 'vuex';
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
const store = new Vuex.Store({
state: {
periods : [],
},
mutations: {
setInitialData (state, payload) {
state.periods = payload;
},
setSelectedOperator(state, payload) {
this.getters.operateurs.forEach( op => {
op.selected = (op.id==payload.id)
})
},
},
getters : {
operators : (state) => {
if (Array.isArray(state.periods))
{
let ops = state.periods
.map( item => {
return item.operators
}).flat();
ops.forEach(op => {
// op.selected=false; //replaced after Radu Diță answer by next line :
if (ops.selected === undefined) op.selected=false;
})
return ops;
}
},
operatorById : (state, getters) => (id) => {
return getters.operators.find(operator => operator.id==id);
},
}
});
import Chrono from './components/Chrono.vue';
var app = new Vue({
el: '#app',
store,
components : { Chrono },
mounted () {
this.$store.commit('setInitialData',
JSON.parse(this.$el.attributes.initialdata.value));
},
computed: {
...mapState(['periods']),
...mapGetters(['operators', 'operatorById']),
selectedOperator(){
this.$store.getters.operators.forEach(op =>{
if (op.selected) return op;
});
return null;
}
},
});
Your getter in vuex for operators is always setting selected to false.
operators : (state) => {
if (Array.isArray(state.periods))
{
let ops = state.periods
.map( item => {
return item.operators
}).flat();
ops.forEach(op => {
op.selected=false;
})
return ops;
}
}
I'm guessing you do this for initialisation, but that's a bad place to put it, as you'll never get a selected operator from that getter. Just move it to the proper mutations. setInitialData seems like the right place.
Finally I found where my problems came from :
The $el.attributes.initialdata.value came from an API and the operator objects it contained didn't have a selected property, so I added it after data was set and it was not reactive.
I just added this property on server side before converting to JSON and sending to Vue, removed the code pointed by Radu Diță since it was now useless, and it works.
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.
vue is throwing this message:
Vue received a Component which was made a reactive object. This can
lead to unnecessary performance overhead, and should be avoided by
marking the component with markRaw or using shallowRef instead of
ref.
<template>
<component v-for="(el, idx) in elements" :key="idx" :data="el" :is="el.component" />
</template>
setup() {
const { getters } = useStore()
const elements = ref([])
onMounted(() => {
fetchData().then((response) => {
elements.value = parseData(response)
})
})
return { parseData }
}
is there a better way to do this?
First, you should return { elements } instead of parseData in your setup i think.
I solved this issue by marking the objects as shallowRef :
import { shallowRef, ref, computed } from 'vue'
import { EditProfileForm, EditLocationForm, EditPasswordForm} from '#/components/profile/forms'
const profile = shallowRef(EditProfileForm)
const location = shallowRef(EditLocationForm)
const password = shallowRef(EditPasswordForm)
const forms = [profile, location, password]
<component v-for="(form, i) in forms" :key="i" :is="form" />
So you should shallowRef your components inside your parseData function. I tried markRaw at start, but it made the component non-reactive. Here it works perfectly.
you could manually shallowCopy the result
<component v-for="(el, idx) in elements" :key="idx" :data="el" :is="{...el.component}" />
I had the same error. I solved it with markRaw. You can read about it here!
my code :
import { markRaw } from "vue";
import Component from "./components/Component.vue";
data() {
return {
Component: markRaw(Component),
}
For me, I had defined a map in the data section.
<script>
import TheFoo from '#/TheFoo.vue';
export default {
name: 'MyComponent',
data: function () {
return {
someMap: {
key: TheFoo
}
};
}
};
</script>
The data section can be updated reactively, so I got the console errors. Moving the map to a computed fixed it.
<script>
import TheFoo from '#/TheFoo.vue';
export default {
name: 'MyComponent',
computed: {
someMap: function () {
return {
key: TheFoo
};
}
}
};
</script>
I had this warning while displaying an SVG component; from what I deduced, Vue was showing the warning because it assumes the component is reactive and in some cases the reactive object can be huge causing performance issues.
The markRaw API tells Vue not to bother about reactivity on the component, like so - markRaw(<Your-Component> or regular object)
I also meet this problem today,and here is my solution to solve it:
setup() {
const routineLayoutOption = reactive({
board: {
component: () => RoutineBoard,
},
table: {
component: () => RoutineTable,
},
flow: {
component: () => RoutineFlow,
},
});
}
I set the component variant as the result of the function.
And in the ,bind it like compoennt()
<component
:is="routineLayoutOption[currentLayout].component()"
></component>
I have typical scenario where I call REST API in vuex actions to fetch some data and then I commit that to mutation.
I use async/await syntax and try/catch/finally blocks. My vuex module looks something like this:
const state = {
users: null,
isProcessing: false,
operationError: null
}
const mutations = {
setOperationError (state, value) {
state.operationError = value
},
setIsProcessing (state, value) {
state.isProcessing = value
if (value) {
state.operationError = ''
}
},
setUsers(state, value) {
state.users= value
}
}
const actions = {
async fetchUsers ({ commit }) {
try {
commit('setIsProcessing', true)
const response = await api.fetchUsers()
commit('setUsers', response.result)
} catch (err) {
commit('setUsers', null)
commit('setOperationError', err.message)
} finally {
commit('setIsProcessing', false)
}
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
Notice that I handle catch(err) { } in vuex action and don’t rethrow that error. I just save error message in the state and then bind it in vue component to show it if operationError is truthy. This way I want to keep vue component clean from error handling code, like try/catch.
I am wondering is this right pattern to use? Is there a better way to handle this common scenario? Should I rethrow error in vuex action and let it propagate to the component?
What I usually do, is have a wrapper around the data being posted, that handles the api requests and stores errors. This way your users object can have the errors recorded on itself and you can use them in the components if any of them are present.
For example:
import { fetchUsers } from '#\Common\api'
import Form from '#\Utils\Form'
const state = {
isProcessing: false,
form: new Form({
users: null
})
}
const mutations = {
setIsProcessing(state, value) {
state.isProcessing = value
},
updateForm(state, [field, value]) {
state.form[field] = value
}
}
const actions = {
async fetchUsers ({ state: { form }, commit }) {
let users = null
commit('setIsProcessing', true)
try {
users = await form.get(fetchUsers);
} catch (err) {
// - handle error
}
commit('updateForm', ['users', users])
commit('setIsProcessing', false)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
Then in the component you can use the errors object on the wrapper like so:
<template>
<div>
<div class="error" v-if="form.erros.has('users')">
{{ form.errors.get('users') }}
</div>
<ul v-if="users">
<li v-for="user in users" :key="user.id">{{ user.username }}</li>
</ul>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
computed: {
...mapState('module' ['form']),
users () {
return this.form.users
}
}
</script>
This is just my personal approach that I find very handy and it served me well up to now. Don't know if there are any standard patterns or if there is an explicit "correct way" to do this.
I like the wrapper approach, because then your errors become automatically reactive when a response from api returns an error.
You can re-use it outside vuex or even take it further and inject the errors into pre-defined error boundaries which act as wrapper components and use the provide/inject methods to propagate error data down the component tree and display them where ever you need them to show up.
Here's an example of error boundary component:
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {
props: {
module: {
type: String,
required: true,
validator: function (value) {
return ['module1', 'module2'].indexOf(value) !== -1
}
},
form: {
type: String,
default: 'form'
}
},
provide () {
return {
errors: this.$store.state[this.module][this.form].errors
}
}
}
</script>
Wrap some part of the application that should receive the errors:
<template>
<div id="app">
<error-boundary :module="module1">
<router-view/>
</error-boundary>
</div>
</template>
Then you can use the errors from the users wrapper in child components like so:
If you have a global error like no response from api and want to display it in the i.e.: sidebar
<template>
<div id="sidebar">
<div v-if="errors.has('global')" class="error">
{{ errors.get('global').first() }}
</div>
...
</div>
</template>
<script>
export default {
inject: [
'errors'
],
...
}
</script>
And the same error object re-used somewhere inside a widget for an error on the users object validation:
<template>
<div id="user-list">
<div v-if="errors.has('users')" class="error">
{{ errors.get('users').first() }}
</div>
...
</div>
</template>
<script>
export default {
inject: [
'errors'
],
...
}
</script>
Jeffrey Way did a series on Vue2 a while ago and he proposed something similar. Here's a suggestion on the Form and Error objects that you can build upon: https://github.com/laracasts/Vue-Forms/blob/master/public/js/app.js
Using Vue TreeSelect Plugin to load a nested list of nodes from firebase backend. It's doc page says,
It's also possible to have root level options to be delayed loaded. If no options have been initially registered (options: null), vue-treeselect will attempt to load root options by calling loadOptions({ action, callback, instanceId }).
loadOptions (in my App.vue) dispatch vuex action_FolderNodesList, fetches (from firebase) formats (as required by vue-treeselect), and mutates the state folder_NodesList, then tries to update options this.options = this.get_FolderNodesList but this does not seems to work.
Here is the loadOptions method (in app.vue)
loadOptions() {
let getFolderListPromise = this.$store.dispatch("action_FolderNodesList");
getFolderListPromise.then(_ => {
this.options = this.get_FolderNodesList;
});
}
Vue errors out with Invalid prop: type check failed for prop "options". Expected Array, got String with value ""
I am not sure what am I doing wrong, why that does not work. A working Codesandbox demo
Source
App.vue
<template>
<div class="section">
<div class="columns">
<div class="column is-7">
<div class="field">
<Treeselect
:multiple="true"
:options="options"
:load-options="loadOptions"
:auto-load-root-options="false"
placeholder="Select your favourite(s)..."
v-model="value" />
<pre>{{ get_FolderNodesList }}</pre>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from "vuex";
import Treeselect from "#riophae/vue-treeselect";
import "#riophae/vue-treeselect/dist/vue-treeselect.css";
export default {
data() {
return {
value: null,
options: null,
called: false
};
},
components: {
Treeselect
},
computed: mapGetters(["get_FolderNodesList"]),
methods: {
loadOptions() {
let getFolderListPromise = this.$store.dispatch("action_FolderNodesList");
getFolderListPromise.then(_ => {
this.options = this.get_FolderNodesList;
});
}
}
};
</script>
Store.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
folder_NodesList: ""
},
getters: {
get_FolderNodesList(state) {
return state.folder_NodesList;
}
},
mutations: {
mutate_FolderNodesList(state, payload) {
state.folder_NodesList = payload;
}
},
actions: {
action_FolderNodesList({ commit }) {
fmRef.once("value", snap => {
var testObj = snap.val();
var result = Object.keys(testObj).reduce((acc, cur) => {
acc.push({
id: cur,
label: cur,
children: recurseList(testObj[cur])
});
return acc;
}, []);
commit("mutate_FolderNodesList", result);
});
}
}
});
Any help is appreciated.
Thanks
It seems you are calling this.options which would update the entire element while only the current expanding option should be updated.
It seems loadOptions() is called with some arguments that you can use to update only the current childnode. The first argument seems to contain all the required assets so I wrote my loadTreeOptions function like this:
loadTreeOptions(node) {
// On initial load, I set the 'children' to NULL for nodes to contain children
// but inserted an 'action' string with an URL to retrieve the children
axios.get(node.parentNode.action).then(response => {
// Update current node's children
node.parentNode.children = response.data.children;
// notify tree to update structure
node.callback();
}).catch(
errors => this.onFail(errors.response.data)
);
},
Then I set :load-options="loadTreeOptions" on the <vue-treeselect> element on the page. Maybe you were only missing the callback() call which updates the structure. My installation seems simpler than yours but it works properly now.
I have created a component which has a function which makes external API calls and then fills an array. I used created() life hook to run the function for the 1st time. I am passing a variable from the parent component into this component and then based upon this variable change I want the function to run again.
How do I achieve this.
Attaching my code below
<template>
<div>
<p>{{ data_to_show_on_mainpage }}</p>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'GetCategoryItemsAndDisplayOne',
props: ['categoriesfordisplay','ismainpage', 'catalogselected'],
data(){
return {
IsMainPage_1 : "",
data_to_show_on_mainpage : [],
}
},
watch: {
catalogselected: function(){
this.GetItemsToShowonMainPage()
}
},
methods:{
changevalue(){
console.log("i am reducing it to emplty after change of catalog")
this.IsMainPage_1 = this.catalogselected
this.data_to_show_on_mainpage = []
},
CatlogService(catlog_name,category,gender,mainpage){
let url = "http://localhost:5000/xyz/" + (this.catalogselected).replace(/'/g,"%27") +"/api/"+ (gender) + "/catalogvis/" + (category) +"/items"
console.log(encodeURI(url))
axios.get(encodeURI(url)).then((resp)=>{
this.data_to_show_on_mainpage.push(resp.data.response.Results.results[0])
})
.catch(err => {
console.log("we got an error the url is " + url)
console.log(err);
})
},
GetItemsToShowonMainPage(){
this.changevalue()
if(this.categoriesfordisplay.men_for_display.length>0){
for(let i =0;i<this.categoriesfordisplay.men_for_display.length;i++){
let category = this.categoriesfordisplay.men_for_display[i].replace(/"/g,"%27");
this.CatlogService(this.catalogselected,category,'men',this.ismainpage)
}
}
if(this.categoriesfordisplay.women_for_display.length>0){
for(let i = 0;i<this.categoriesfordisplay.women_for_display.length;i++){
let category = this.categoriesfordisplay.women_for_display[i].replace(/"/g,"");
this.CatlogService(this.catalogselected,category,'women',this.ismainpage)
}
}
},
},
created(){
this.GetItemsToShowonMainPage()
}
}
</script>
<style>
</style>
How Do i trigger the GetItemsToShowonMainPage() function whenever the catalogselected varaible is changed.
It looks fine.
As #a-lau says, make sure the parent is updating the catalogselected prop
Btw, you can write your watcher this way and remove completely the created hook:
watch: {
catalogselected: {
handler: "GetItemsToShowonMainPage",
immediate: true
}
}
If you still have issues you might want to write a minimal reproduction on https://codesandbox.io/s/vue