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.
Related
I am new to Vue and stuck. I am trying to send user input data from a form into a vuex store. From that vuex store, an action will be called (fetching from API) and I would like that data back into my app and components.
<template>
<div>
<h1>APP NAME</h1>
<form action="submit" #submit.prevent="sendCityName()">
<label for="query"></label>
<input
type="text"
id="query"
v-model="cityName"
>
<button type="submit">Submit</button>
</form>
<h3>{{ lat }}</h3>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
data() {
return {
cityName: ''
}
},
computed: {
coordinates () {
return this.$store.state.lat
}
},
methods: {
sendCityName() {
this.$store.commit('fetchCity', this.cityName)
}
},
}
</script>
Here is my index.vue and getting the error "Cannot read properties of undefined (reading 'commit')"
here is my store.js. I want to use the lat and lon across my app.
export const state = () => ({
lat: '',
lon: ''
})
export const mutations = {
SET_LAT(state, payload){
state.lat = payload
},
SET_LON(state, payload){
state.lon = payload
}
}
export const actions = {
async fetchCity({ commit }, cityName) {
// make request
axios.get(
`https://api.openweathermap.org/geo/1.0/direct`, {
params: {
appid: "xxxxxxx",
q: cityName,
}
}).then((response) => {
commit('SET_LAT', response.data[0].lat);
commit('SET_LON', response.data[0].lng);
});
},
};
When I button submit I get the error "Cannot read properties of undefined (reading 'commit')"
Here is my working repo with the fixes mentioned below.
There are 3 things in your code:
remove vuex from package.json and run yarn again, that one is already baked into Nuxt as stated in the official documentation, those are the only steps needed
all the files inside of store will be namespaced by default for you, since you do have store/store.js, the proper syntax will be
async sendCityName() {
await this.$store.dispatch('store/fetchCity', this.cityName) // 👈🏻 store prefix
}
since you do use the axios module, you should have the following in your action (using the async/await syntax since it's more modern and preferable)
async fetchCity({ commit }, cityName) {
const response = await this.$axios.get(
`https://api.openweathermap.org/geo/1.0/direct`, {
params: {
appid: "3d91ba5b3c11d13158a2726aab902a0b",
q: cityName,
}
})
commit('SET_LAT', response.data[0].lat)
commit('SET_LON', response.data[0].lng)
}
Looking at the browser's console, you also have some errors to fix.
I can also recommend an ESlint + Prettier configuration so that you keep your code error-proof + properly formatted at all times.
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 try to find a way to use vuex with reusable component which store data in a store. The thing is, I need the store to be unique for each component instance.
I thought Reusable module of the doc was the key but finally it doesn't seem to be for this purpose, or i didn't understand how to use it.
The parent component:
(the prop “req-path” is used to pass different URL to make each FileExplorer component commit the action of fetching data from an API, with that url path)
<template>
<div class="container">
<FileExplorer req-path="/folder/subfolder"></FileExplorer>
<FileExplorer req-path="/anotherfolder"></FileExplorer>
</div>
</template>
<script>
import { mapState, mapGetters } from "vuex";
import FileExplorer from "#/components/FileExplorer.vue";
export default {
components: {
FileExplorer
}
};
</script>
The reusable component:
<template>
<div class="container">
<ul v-for="(item, index) in folderIndex" :key="index">
<li>Results: {{ item.name }}</li>
</ul>
</div>
</div>
</template>
<script>
import { mapState, mapGetters } from "vuex";
export default {
props: ["reqPath"],
},
computed: {
...mapState("fileExplorer", ["folderIndex"])
},
created() {
// FETCH DATA FROM API
this.$store
.dispatch("fileExplorer/indexingData", {
reqPath: this.reqPath
})
.catch(error => {
console.log("An error occurred:", error);
this.errors = error.response.data.data;
});
}
};
</script>
store.js where I invoke my store module that I separate in different files, here only fileExplorer module interest us.
EDIT : I simplified the file for clarity purpose but I have some other state and many mutations inside.
import Vue from 'vue'
import Vuex from 'vuex'
// Import modules
import { fileExplorer } from '#/store/modules/fileExplorer'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
fileExplorer,
…
}
})
#/store/modules/fileExplorer.js
import ApiService from "#/utils/ApiService"
export const fileExplorer = ({
namespaced: true,
state: {
folderIndex: {},
},
mutations: {
// Called from action (indexingData) to fetch folder/fil structure from API
SET_FOLDERS_INDEX(state, data) {
state.folderIndex = data.indexingData
},
actions: {
// Fetch data from API using req-path as url
indexingData({
commit
}, reqPath) {
return ApiService.indexingData(reqPath)
.then((response) => {
commit('SET_FOLDERS_INDEX', response.data);
})
.catch((error) => {
console.log('There was an error:', error.response);
});
}
}
});
I need each component to show different data from those 2 different URL, instead i get the same data in the 2 component instance (not surprising though).
Thanks a lot for any of those who read all that !
Module reuse is about when you are creating multiple modules from the same module config.
First, use a function for declaring module state instead of a plain object.
If we use a plain object to declare the state of the module, then that
state object will be shared by reference and cause cross store/module
state pollution when it's mutated.
const fileExplorer = {
state () {
return {
folderIndex: {}
}
},
// mutations, actions, getters...
}
Then, dynamically register a new module each time a new FileExplorer component is created and unregister that module before the component is destroyed.
<template>
<div class="container">
<ul v-for="(item, index) in folderIndex" :key="index">
<li>Results: {{ item.name }}</li>
</ul>
</div>
</div>
</template>
<script>
import { fileExplorer } from "#/store/modules/fileExplorer";
import store from "#/store/index";
var uid = 1
export default {
props: ["reqPath"],
data() {
return {
namespace: `fileExplorer${uid++}`
}
},
computed: {
folderIndex() {
return this.$store.state[this.namespace].folderIndex
}
},
created() {
// Register the new module dynamically
store.registerModule(this.namespace, fileExplorer);
// FETCH DATA FROM API
this.$store
.dispatch(`${this.namespace}/indexingData`, {
reqPath: this.reqPath
})
.catch(error => {
console.log("An error occurred:", error);
this.errors = error.response.data.data;
});
},
beforeDestroy() {
// Unregister the dynamically created module
store.unregisterModule(this.namespace);
}
};
</script>
You no longer need the static module registration declared at store creation.
export default new Vuex.Store({
modules: {
// fileExplorer, <-- Remove this static module
}
})
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
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.