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.
Related
since yesterday I'm struggling with creating a Vue mixin with a parameter and I'm getting a [Vue warn]: Failed to mount component: template or render function not defined.
Here is my JS file including mixin:
export default (dataObject) => ({
data() {
return {
inputValue: ''
}
},
methods: {
updateValue(newValue) {
this.inputValue = newValue
}
},
mounted() {
this.$bus.$on('value-changed', this.updateValue)
},
beforeDestroy() {
this.$bus.$off('value-changed');
},
computed: {
filteredData() {
if (this.inputValue !== '') {
let newData = Object.keys(dataObject)
.filter(key => key.includes(this.inputValue))
.reduce(
(newData, current) => ((newData[current] = dataObject[current]), newData), {}
)
return newData
}
else return dataObject
}
}
})
And here is my Vue component:
import searchLogic from '../logic/searchLogic.js'
import { mapState } from 'vuex'
export default {
computed: {
...mapState(['champions']),
},
mixins: [searchLogic(this.champions)]
}
Importing this file works, because when I try to import a normal mixin without arguments it works properly. I also tried passing champions and "champions" instead of this.champions but none seem to work. Is it some problem with a mixin? I read that it's possible to return a function returning an object to use parameters when creating mixins.
Based on this article you should be able to statically pass a parameter, but not dynamically.
https://forum.vuejs.org/t/pass-parameters-to-a-mixin-method/26401/3
However, what I can see from the warn you get and the code you shared, is that you haven't defined any template for the component using the template property or the template element in the .vue file.
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.
Okay, so I've been bumping my head on the walls trying to figure out what the hell's going on here. See, I've been trying to load a module dynamically in Vue.js. What confuses me here is that the following actually works (i.e. when I hardcode the path to my module):
currentModal(){
return () => System.import(`../../ride/modals/boarding.vue`);
},
However, if I do this:
currentModal(){
let path = "../../ride/modals/";
return () => System.import(`${path}boarding.vue`);
},
I get the following error:
Cannot find module '../../ride/modals/boarding.vue'.
Then, if I go the other way around and do:
currentModal(){
let content = "boarding.vue";
return () => System.import(`../../ride/modals/${content}`);
},
The error becomes:
Error in render: "TypeError: undefined is not a function"
Obviously, something's different when I use a variable instead of hardcoding the path... but for the life of me, I can't figure out what. Anyone has any clue?
As Bergur mentioned in his comment, the problem is indeed that webpack cannot resolve a string that doesn't yet exist (issue explained here). What I did instead is add a property to my component that I called "content-list" and in the main page, I fill it with my desired components like so:
parent component:
<template>
.....
<modal-window
:init-visible="modalVisible"
:content-list="contentList"
#modalClosed="modalClosed">
</modal-window>
.....
</template>
<script>
import mainContent from './modals/main.vue'; //I doubt this is still useful
import otherContent from './modals/other.vue'; //I doubt this is still useful
export default{
data(){
return {
modalVisible: false,
contentList: {
main: () => System.import("./modals/main.vue"),
other: () => System.import("./modals/other.vue")
},
}
},
}
</script>
Inside the modal component:
<template>
...
<div ref="scrolling-pane" class="scrolling-pane" :class="{ 'scrolling-pane-normal': !isDirty, 'scrolling-pane-dirty': isDirty }" key="b">
<transition name="content-slide"
:enter-active-class="enterClassName"
:leave-active-class="leaveClassName"
#before-enter="beforeEnter"
#before-leave="beforeLeave"
#after-enter="afterEnter"
#after-leave="afterLeave">
<keep-alive>
<component :is="currentModal" #switchContent="onSwitchContent"></component>
</keep-alive>
</transition>
</div>
...
</template>
<script>
export default{
components: {
},
props: {
initVisible: Boolean,
contentList: Object
},
data(){
return {
currentContent: this.contentList['main']
}
}
...
</script>
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.
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.