vue js 2 : how to communicate between store and a component - javascript

I am a beginner with vue js.
i m trying to handle errors from component and display it on other component.
Apparently handling work becase i can see data in my store
With a props to my component (error.vue), it handle it in the data variable.
But after that it can t display it on my vue .
Why ?
Here is my code :
My store is :
var store = new Vuex.Store(
{
state: {
errors:{}
},
mutations: {
setErrors(state, error) {
for(var err in error) {
state.errors[err] = error[err]
}
}
}
})
my vue where i put my error component:
<template>
<div id="wrapper">
<div class="container">
<error_handling></error_handling>
<router-view></router-view>
</div>
</div>
</template>
<script>
import Error from './components/Error.vue'
import store from './store';
export default {
components: {
'error_handling': Error
},
data() {
return {
erreurs: store.state.errors
}
}
}
</script>
my error vue :
<template>
<div>
<ul>
{{errors_handling}}
<!--<li v-for="error in errors_handling">{{ error }}</li>-->
</ul>
</div>
</template>
<script>
export default {
props: ['errors_hand'],
data() {
return {
errors_handling: this.errors_hand
}
}
}
</script>

Based on your provided code.
You are getting a state of "errors"
You are not committing a mutation
By not committing a mutation, you are not changing a state
Docs: Vuex Mutations

Store.js
var store = new Vuex.Store(
{
state: {
errors:{}
},
mutations: { // Change the state object
setErrors(state, error) {
for(var err in error) {
state.errors[err] = error[err]
}
}
},
getters:{
// getters gets the current object of state
GetErrors(state) //by default getters get state as first paramater
{
return state.errors;
}
},
})
Error Component
<script>
export default {
computed:{
errors_handling(){
return this.$store.getters.GetErrors;
}
},
methods:{
//commit your mutation or dispatch when using action
ChangeState(error){
this.$store.commiit('setErrors',error);
}
}
}
</script>
But you must use actions to run it asyncronously

I would use a bus to pass errors from wherever they occur to the error component. This way the error component need not interact with your store or any other component directly, and can manage its own internal state easily. You also would not need to include the error component in any other component.
This example assumes that you are wanting only a single Error Component instance in your UI. I would put the error component instance in your main App template and have it show or hide itself based on whether it has any non-handled errors.
To declare a simple bus...
in file errorBus.js
import Vue from 'vue'
const errorBus = new Vue();
export default {
errorBus
}
Wherever an error occurs that you want to pass to the error component, use...
import errorBus from './errorBus.js'
errorBus.errorBus.$emit("notifyError", { msg: 'An error has occurred'});
In the error component...
import errorBus from './errorBus.js'
and within the component definition...
created: function() {
errorBus.errorBus.$on("notifyError", function(error) {this.addError(error)};
},
data () {
return {
errors: []
};
},
methods: {
addError: function(error) {
this.errors.push(error);
}
}
With this mechanism in place, you could easily handle different errors in different ways by passing additional information in the error object - for example, you could add {handling: "toast", priority: 0} which would cause the error component to immediately toast the error.
If you use this to toast, consider having the errors remain for later viewing after the error is toasted - I have always wanted something like an error drawer that I could open at my leisure instead of having to handle a toast immediately before it disappears.

Related

problem with using vue.js routing and mixin

Vue developers.
Now I'm trying vuebnb tutorial and have a problem with routing and mixins.
I tried to assign data before entering the router using beforeRouteEnter guard, but it seems like my template is rendered before data assign.
Following is the code I tried.
ListingPage.vue
<template>
<div>
<img :src="'/' + images[1].img_path" />
</div>
</template>
<script>
import { populateAmenitiesAndPrices } from "../js/helpers";
import routeMixin from '../js/route-mixin';
export default {
mixins: [ routeMixin ],
data() {
return {
title: null,
about: null,
address: null,
amenities: [],
prices: [],
images:[],
}
},
methods: {
assignData({ listing, images }) {
console.log('inside_component_before_assign');
this.images = [...images];
Object.assign(this.$data, populateAmenitiesAndPrices(listing));
console.log('inside_component_after_assign');
}
},
components: {
}
};
</script>
route-mixin.js
import axios from 'axios';
function getData(to) {
return new Promise((resolve) => {
let serverData = JSON.parse(window.vuebnb_data);
if (!serverData.path || to.path !== serverData.path) {
axios.get(`/api${to.path}`).then(({ data }) => {
resolve(data);
});
} else {
resolve(serverData);
}
});
}
export default {
beforeRouteEnter: (to, from, next) => {
console.log('before_next');
getData(to).then((data) => {
next(component => {
component.assignData(data);
});
});
console.log('after_next');
}
};
Here data is a object and it is like {listing: {...}, images: Array(5), path: "/listing/1}.
Data fetch from the server is checked.
But when I try to render ListingPage.vue, error is logged in console like this:
*
[Vue warn]: Error in render: "TypeError: Cannot read property 'img_path' of undefined"
found in
---> <ListingPage> at resources/assets/components/ListingPage.vue
<App> at resources/assets/components/App.vue
<Root>
Regardless of the error, the page is displayed successfully. Please help me to get rid of this error:
From the reply on your comment I conclude your problem is solved by adding a v-if to your html tag?
Changing your code to
<img v-if="images[1]" :src="'/' + images[1].img_path" />
should work, as the async. request is probably not resolved by the time, the page is compiled by vue. For that have a look at the vue component lifecycle.
As the data attributes are reactive, your component will be visible and the value within will be updated couple ms later than it is compiled.
I'd be glad if somebody could suggest another approach as do not if this is the best practice myself.

Wait for VueX value to load, before loading component

When a user tries to directly navigate load a component url, an http call is made in my vuex actions, which will define a value in my state once it resolves.
I don't want to load my component until the http call is resolved, and the state value is defined.
For Example, in my component
export default {
computed: {
...mapState({
// ** this value needs to load before component mounted() runs **
asyncListValues: state => state.asyncListValues
})
},
mounted () {
// ** I need asyncListValues to be defined before this runs **
this.asyncListValues.forEach((val) => {
// do stuff
});
}
}
How can I make my component wait for asyncListValues to load, before loading my component?
One way to do it is to store state values.
For example, if your store relies on single API, you would do something like this. However, for multiple APIs, it's a good idea to store each api load state individually, or using a dedicated object for each API.
There are usualy 4 states that you can have, which I prefer to have in a globally accessible module:
// enums.js
export default {
INIT: 0,
LOADING: 1,
ERROR: 2,
LOADED: 3
};
Then, you can have the variable stored in the vuex state, where the apiState is initialized with INIT. you can also initialize the array with [], but that shouldn't be necessary.
import ENUM from "#/enums";
// store.js
export default new Vuex.Store({
state: {
apiState: ENUM.INIT,
accounts: [],
// ...other state
},
mutations: {
updateAccounts (state, accounts) {
state.accounts = accounts;
state.apiState = ENUM.LOADED;
},
setApiState (state, apiState) {
state.apiState = apiState;
},
},
actions: {
loadAccounts ({commit) {
commit('setApiState', ENUM.LOADING);
someFetchInterface()
.then(data=>commit('updateAccounts', data))
.catch(err=>commit('setApiState', ENUM.ERROR))
}
}
});
Then, by adding some computed variables, you can toggle which component is shown. The benefit of using state is that you can easily identify the Error state, and show a loading animation when state is not ready.
<template>
<ChildComponent v-if="apiStateLoaded"/>
<Loader v-if="apiStateLoading"/>
<Error v-if="apiStateError"/>
</template>
<script>
import ENUM from "#/enums";
export default {
computed: {
...mapState({
apiState: state=> state.apiState
}),
apiStateLoaded() {
return this.apiState === ENUM.LOADED;
},
apiStateLoading() {
return this.apiState === ENUM.LOADING || this.apiState === ENUM.INIT;
},
apiStateError() {
return this.apiState === ENUM.ERROR;
},
})
}
</script>
aside... I use this pattern to manage my applications as a state machine. While this example utilizes vuex, it can be adapted to use in a component, using Vue.observable (vue2.6+) or ref (vue3).
Alternatively, if you just initialize your asyncListValues in the store with an empty array [], you can avoid errors that expect an array.
Since you mentioned vue-router in your question, you can use beforeRouteEnter which is made to defer the rendering of a component.
For example, if you have a route called "photo":
import Photo from "../page/Photo.vue";
new VueRouter({
mode: "history",
routes: [
{ name: "home", path: "/", component: Home },
{ name: "photo", path: "/photo", component: Photo }
]
});
You can use the beforeRouteEnter like this:
<template>
<div>
Photo rendered here
</div>
</template>
<script>
export default {
beforeRouteEnter: async function(to, from, next) {
try {
await this.$store.dispatch("longRuningHttpCall");
next();
} catch(exception) {
next(exception);
}
}
}
</script>
What it does is, waiting for the action to finish, updating your state like you want, and then the call to next() will tell the router to continue the process (rendering the component inside the <router-view></router-view>).
Tell me if you need an ES6-less example (if you do not use this syntax for example).
You can check the official documentation of beforeRouteEnter on this page, you will also discover you can also put it at the route level using beforeEnter.
One approach would be to split your component into two different components. Your new parent component could handle fetching the data and rendering the child component once the data is ready.
ParentComponent.vue
<template>
<child-component v-if="asyncListValues && asyncListValues.length" :asyncListValues="asyncListValues"/>
<div v-else>Placeholder</div>
</template>
<script>
export default {
computed: {
...mapState({
asyncListValues: state => state.asyncListValues
})
}
}
</script>
ChildComponent.vue
export default {
props: ["asyncListValues"],
mounted () {
this.asyncListValues.forEach((val) => {
// do stuff
});
}
}
Simple way for me:
...
watch: {
vuexvalue(newVal) {
if (newVal == 'XXXX')
this.loadData()
}
}
},
computed: {
...mapGetters(['vuexvalue'])
}
Building on some of the other answers, if you're using Router, you can solve the problem by only calling RouterView when the state has been loaded.
Start with #daniel's approach of setting a stateLoaded flag when the state has been loaded. I'll just keep it simple here with a two-state flag, but you can elaborate as you like:
const store = createStore({
state () {
return {
mysettings: {}, // whatever state you need
stateLoaded: false,
}
},
mutations: {
set_state (state, new_settings) {
state.settings = new_settings;
state.stateLoaded = true;
},
}
}
Then, in app.vue you'll have something like this:
<div class="content">
<RouterView/>
</div>
Change this to:
<div class="content">
<RouterView v-if="this.$store.state.stateLoaded"/>
</div>
The v-if won't even attempt to do anything with RouterView until the (reactive) stateLoaded flag goes true. Therefore, anything you're rendering with the Router won't get called, and so there won't be any undefined state variables in it when it does get loaded.
You can of course build on this with a v-else to perhaps show a "Loading..." screen or something, just in case the state loading takes longer than expected. Using #daniel's multi-state flag, you could even report if there was a problem loading the state, and offer a Retry button or something.

Uncaught error when trying to embed a Vega chart in Vue + Vuex project

I am trying to use vega-embed within a Vue.js (together with vuex for state management) project. Basically the backend serves a Vega json object which is picked up by the frontend via HTTP GET request with a click event. However I have to click twice to get the plot displayed and the first click event always triggers an error "Uncaught (in promise) TypeError: Cannot read property '$schema' of null". Can someone help me debug? Very much appreciated. Details shown below:
The vue component file:
<template>
<button #click.native="fetchCars(); displayVegaPlot()">fetch cars</button>
<div id="vega-example"></div>
</template>
<script>
import {default as vegaEmbed} from 'vega-embed'
import {
mapState
} from 'vuex'
export default {
name: 'VegaExample',
props: {
component_msg: String
},
methods: {
fetchCars () {
this.$store.dispatch('fetchCars')
},
displayVegaPlot () {
vegaEmbed('#vega-example', this.vega_cars, {actions: false})
}
},
computed: {
...mapState([
'vega_cars'
])
}
}
</script>
... and the store js file:
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
Vue.use(Vuex)
export default new Vuex.Store({
strict: true,
state: {
error: '',
vega_cars: null
},
mutations: {
SET_CARS: (state, cars) => {
state.vega_cars = cars
},
SET_ERROR: (state, error) => {
state.error = error
}
}
actions: {
fetchCars: (context) => {
axios.get(`vega_cars`)
.then(response => context.commit('SET_CARS', response.data))
.catch(error => context.commit('SET_ERROR', error))
}
}
Thanks a lot for the comment from #TommyF, after some doc reading I think I figured out the solution (apparently as a newcomer to web app dev and Vue I didn't know Vue offers a special watch facility). So in the component Vue file, instead of declaring a method displayVegaPlot to call imperatively, a watch can be setup to do the display the Vega plot, as soon as vega_cars changes value:
watch: {
vega_cars: (spec) => {
console.log('$store.state.vega_cars changed value')
if (spec) {
vegaEmbed('#vega-example', spec, {actions: false})
}
}
}
and of course ...mapState(['vega_cars']) needs to be put in computed.

Property Undefined on Vue Component

Getting an error that project is undefined when trying to render out some text using {{project.projectSummary}} in my ProjectContainer component. I'm guessing when the component is initialized project is undefined because it shows up in VueTools fine on my component.
So next, I try to set v-if="resource.project" on the parent component
<template v-if="resource.project">
<div class="resourceContainer">
<resource-card :resource="resource" :showResourceBtn="showResourceBtn"></resource-card>
<project-container :project="resource.project"></project-container>
</div>
</template>
<script>
import { firebaseAuth, database } from '../../database/database'
import Vue from 'vue'
import VueFire from 'vuefire'
import ResourceCard from '../ResourceCard/ResourceCard'
import ProjectContainer from './Project'
Vue.use(VueFire);
export default {
name: 'Resource',
components: {
ResourceCard,
ProjectContainer
},
data () {
return {
showResourceBtn: true
}
},
computed: {
fbUser () {
return firebaseAuth.currentUser
}
},
created () {
// Load resource
this.$bindAsObject('resource', database.ref('/resources/' + this.$route.params.resourceKey));
}
}
</script>
But this isn't working either. Gives me a warning in the console that Invalid prop: type check failed for prop "project". Expected Object, got Undefined.
Any thoughts? Thanks

How to design a store in Vuex to handle clicks in nested, custom components?

I'm trying to design a store to manage the events of my Vuex application. This far, I have the following.
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
const state = { dataRows: [], activeDataRow: {} };
const mutations = {
UPDATE_DATA(state, data) { state.dataRows = data; state.activeDataRow = {}; },
};
export default new Vuex.Store({ state, mutations });
I'm going to have a number of list items that are supposed to change the value of the data in the store when clicked. The design of the root component App and the menu bar Navigation is as follows (there will be a bunch of actions in the end so I've collected them in the file actions.js).
<template>
<div id="app">
<navigation></navigation>
</div>
</template>
<script>
import navigation from "./navigation.vue"
export default { components: { navigation } }
</script>
<template>
<div id="nav-bar">
<ul>
<li onclick="console.log('Clickaroo... ');">Plain JS</li>
<li #click="updateData">Action Vuex</li>
</ul>
</div>
</template>
<script>
import { updateData } from "../vuex_app/actions";
export default {
vuex: {
getters: { activeDataRow: state => state.activeDataRow },
actions: { updateData }
}
}
</script>
Clicking on the first list item shows the output in the console. However, when clicking on the second one, there's nothing happening, so I'm pretty sure that the event isn't dispatched at all. I also see following error when the page's being rendered:
Property or method "updateData" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
I'm very new to Vuex so I'm only speculating. Do I need to put in reference to the updateData action in the store, alongside with state and mutations? How do I do that? What/where's the "data option" that the error message talks about? Isn't it my components state and it's properties?
Why the error
You are getting the error, because when you have <li #click="updateData"> in the template, it looks for a method updateData in the vue component which it does not find, so it throws the error. To resolve this, you need to add corresponding methods in the vue component like following:
<script>
import { updateData } from "../vuex_app/actions";
export default {
vuex: {
getters: { activeDataRow: state => state.activeDataRow },
actions: { updateData }
},
methods:{
updateData: () => this.$store.dispatch("updateData")
}
}
</script>
What this.$store.dispatch("updateData") is doing is calling your vuex actions as documented here.
What/where's the "data option"
You don't have any data properties defined, data properties for a vue component can be used, if you want to use that only in that component. If you have data which needs to be accessed across multiple components, you can use vuex state as I believe you are doing.
Following is the way to have data properties for a vue component:
<script>
import { updateData } from "../vuex_app/actions";
export default {
date: {
return {
data1 : 'data 1',
data2 : {
nesteddata: 'data 2'
}
}
}
vuex: {
getters: { activeDataRow: state => state.activeDataRow },
actions: { updateData }
},
methods:{
updateData: () => this.$store.dispatch("updateData")
}
}
</script>
You can use these data properties in the views, have computed properies based on it, or create watchers on it and many more.

Categories

Resources