I have the problem that my UI is somehow not updating after the applications fetches some data from the sever backend.
I have the following code:
<template>
<div v-if="restaurant">
<div class="center logo-container">
<img class="img-fit" v-bind:src="'/api/restaurant/logo/' + restaurant.id" alt=""/>
</div>
<h2 class="title center dm-text-header">{{ restaurant.name }}</h2>
<h4 class="subheading center">{{ restaurant.address.street }}, {{ restaurant.address.city }}</h4>
</div>
</template>
<script lang="ts">
import axios from 'axios';
import { Options, Vue } from "vue-class-component";
import { Tag } from "./Tag";
import { Restaurant } from "./Restaurant";
#Options({
props: {
}
})
export default class Menu extends Vue {
// hardcoded for testing
restaurantId = "8ykw9ljq";
tagUrl = "/api/menu/" + this.restaurantId + "/tags";
restaurantUrl = "/api/restaurant/" + this.restaurantId;
restaurant!: Restaurant;
tags: Tag[] = [];
mounted() {
// get tags
this.getTags();
// get restaurant
this.getRestaurant();
}
getRestaurant(): void {
axios.get<Restaurant>(this.restaurantUrl)
.then(res => {
this.restaurant = res.data;
});
}
getTags(): void {
axios.get(this.tagUrl)
.then(res => {
this.tags = res.data;
});
}
}
</script>
I verified that the backend actually serves the correct restaurant and logged the results after the axios call finishes. The problem is that the DOM is not updated. If I add the following to the the DOM is upadated:
<template>
...
<div>
{{tags}}
</div>
<template>
It seems to me that vue somehow only updated the DOM if it recognizes changes to the already initialized empty array but not the currently uninitialized restaurant object.
I further get a warning:
[Vue warn]: Property "restaurant" was accessed during render but is not defined on instance. on the v-if what I kind of find strange because that is the exact reason it is there. How do I need to initialize the restaurant, such that the update through axios is correctly recognized by vue?
Try a Typescript union with null:
restaurant: Restaurant | null = null;
From the Vue Class Component docs:
Note that if the initial value is undefined, the class property will not be reactive which means the changes for the properties will not be detected
and
To avoid this, you can use null value or use data hook instead:
import Vue from 'vue'
import Component from 'vue-class-component'
#Component
export default class HelloWorld extends Vue {
// `message` will be reactive with `null` value
message = null
// See Hooks section for details about `data` hook inside class.
data() {
return {
// `hello` will be reactive as it is declared via `data` hook.
hello: undefined
}
}
}
Related
I am terribly new to Vue, so forgive me if my terminology is off. I have a .NET Core MVC project with small, separate vue pages. On my current page, I return a view from the controller that just has:
#model long;
<div id="faq-category" v-bind:faqCategoryId="#Model"></div>
#section Scripts {
<script src="~/scripts/js/faqCategory.js"></script>
}
Where I send in the id of the item this page will go grab and create the edit form for. faqCategory.js is the compiled vue app. I need to pass in the long parameter to the vue app on initialization, so it can go fetch the full object. I mount it with a main.ts like:
import { createApp } from 'vue'
import FaqCategoryPage from './FaqCategoryPage.vue'
createApp(FaqCategoryPage)
.mount('#faq-category');
How can I get my faqCategoryId into my vue app to kick off the initialization and load the object? My v-bind attempt seems to not work - I have a #Prop(Number) readonly faqCategoryId: number = 0; on the vue component, but it is always 0.
My FaqCategoryPAge.vue script is simply:
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { Prop } from 'vue-property-decorator'
import Card from "#/Card.vue";
import axios from "axios";
import FaqCategory from "../shared/FaqCategory";
#Options({
components: {
Card,
},
})
export default class FaqCategoryPage extends Vue {
#Prop(Number) readonly faqCategoryId: number = 0;
mounted() {
console.log(this.faqCategoryId);
}
}
</script>
It seems passing props to root instance vie attributes placed on element the app is mounting on is not supported
You can solve it using data- attributes easily
Vue 2
const mountEl = document.querySelector("#app");
new Vue({
propsData: { ...mountEl.dataset },
props: ["message"]
}).$mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app" data-message="Hello from HTML">
{{ message }}
</div>
Vue 3
const mountEl = document.querySelector("#app");
Vue.createApp({
props: ["message"]
}, { ...mountEl.dataset }).mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0/vue.global.js"></script>
<div id="app" data-message="Hello from HTML">
{{ message }}
</div>
Biggest disadvantage of this is that everything taken from data- attributes is a string so if your component expects something else (Number, Boolean etc) you need to make conversion yourself.
One more option of course is pushing your component one level down. As long as you use v-bind (:counter), proper JS type is passed into the component:
Vue.createApp({
components: {
MyComponent: {
props: {
message: String,
counter: Number
},
template: '<div> {{ message }} (counter: {{ counter }}) </div>'
}
},
}).mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0/vue.global.js"></script>
<div id="app">
<my-component :message="'Hello from HTML'" :counter="10" />
</div>
Just an idea (not a real problem)
Not really sure but it can be a problem with Props casing
HTML attribute names are case-insensitive, so browsers will interpret any uppercase characters as lowercase. That means when you're using in-DOM templates, camelCased prop names need to use their kebab-cased (hyphen-delimited) equivalents
Try to change your MVC view into this:
<div id="faq-category" v-bind:faq-category-id="#Model"></div>
Further to Michal LevĂ˝'s answer regarding Vue 3, you can also implement that pattern with a Single File Component:
app.html
<div id="app" data-message="My Message"/>
app.js
import { createApp } from 'vue';
import MyComponent from './my-component.vue';
const mountEl = document.querySelector("#app");
Vue.createApp(MyComponent, { ...mountEl.dataset }).mount("#app");
my-component.vue
<template>
{{ message }}
</template>
<script>
export default {
props: {
message: String
}
};
</script>
Or you could even grab data from anywhere on the parent HTML page, eg:
app.html
<h1>My Message</h1>
<div id="app"/>
app.js
import { createApp } from 'vue';
import MyComponent from './my-component.vue';
const message = document.querySelector('h1').innerText;
Vue.createApp(MyComponent, { message }).mount("#app");
my-component.vue
<template>
{{ message }}
</template>
<script>
export default {
props: {
message: String
}
};
</script>
To answer TheStoryCoder's question: you would need to use a data prop. My answers above demonstrate how to pass a value from the parent DOM to the Vue app when it is mounted. If you wanted to then change the value of message after it was mounted, you would need to do something like this (I've called the data prop myMessage for clarity, but you could also just use the same prop name message):
<template>
{{ myMessage }}
<button #click="myMessage = 'foo'">Foo me</button>
</template>
<script>
export default {
props: {
message: String
},
data() {
return {
myMessage: this.message
}
}
};
</script>
So I'm not at all familiar with .NET and what model does, but Vue will treat the DOM element as a placeholder only and it does not extend to it the same functionality as the components within the app have.
so v-bind is not going to work, even without the value being reactive, the option is not there to do it.
you could try a hack to access the value and assign to a data such as...
const app = Vue.createApp({
data(){
return {
faqCategoryId: null
}
},
mounted() {
const props = ["faqCategoryId"]
const el = this.$el.parentElement;
props.forEach((key) => {
const val = el.getAttribute(key);
if(val !== null) this[key] = (val);
})
}
})
app.mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.11/dist/vue.global.prod.js"></script>
<div id="app" faqCategoryId="12">
<h1>Faq Category Id: {{faqCategoryId}}</h1>
</div>
where you get the value from the html dom element, and assign to a data. The reason I'm suggesting data instead of props is that props are setup to be write only, so you wouldn't be able to override them, so instead I've used a variable props to define the props to look for in the dom element.
Another option
is to use inject/provide
it's easier to just use js to provide the variable, but assuming you want to use this in an mvc framework, so that it is managed through the view only. In addition, you can make it simpler by picking the exact attributes you want to pass to the application, but this provides a better "framework" for reuse.
const mount = ($el) => {
const app = Vue.createApp({
inject: {
faqCategoryId: {
default: 'optional'
},
},
})
const el = document.querySelector($el)
Object.keys(app._component.inject).forEach(key => {
if (el.getAttribute(key) !== null) {
app.provide(key, el.getAttribute(key))
}
})
app.mount('#app')
}
mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.11/dist/vue.global.prod.js"></script>
<div id="app" faqCategoryId="66">
<h1>Faq Category Id: {{faqCategoryId}}</h1>
</div>
As i tried in the following example
https://codepen.io/boussadjra/pen/vYGvXvq
you could do :
mounted() {
console.log(this.$el.parentElement.getAttribute("faqCategoryId"));
}
All other answers might be valid, but for Vue 3 the simple way is here:
import {createApp} from 'vue'
import rootComponent from './app.vue'
let rootProps = {};
createApp(rootComponent, rootProps)
.mount('#somewhere')
I try to get the state from the store using the mapState function, But I can't use the generated code that returns the values into my template code ...
<template>
// Some code
<template v-if="!isLoggedIn">
// Some code
</template>
<template v-else>
// Some code
{{ currentUser.name }}
</template>
// Some code
</template>
<script>
import { mapState } from "vuex";
export default {
// Some code
computed: {
...mapState({auth : ['currentUser', 'isLoggedIn','customers']})
}
}
</script>
instead the following code work
<script>
import { mapState } from "vuex";
export default {
// Some code
computed: {
currentUser() {
return this.$store.state.auth.currentUser
},
isLoggedIn() {
return this.$store.state.auth.isLoggedIn
},
}
}
</script>
Warning message
[Vue warn]: Property or method "isLoggedIn" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. See: https://v2.vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.
Thanks in advance
The right syntax to access non-root properties is the following (using arrow functions) :
computed: {
...mapState({
currentUser: state => state.auth.currentUser,
isLoggedIn: state => state.auth.isLoggedIn,
customers: state => state.auth.customers
})}
Check the documentation.
If you're trying to access values from a namespaced vuex module called auth, pass the name of the module as the first argument and the array of values to map as the second argument:
computed: {
...mapState('auth', ['currentUser', 'isLoggedIn','customers'])
}
You can mapState the module and then use, say this.auth.isLoggedin
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.
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.
I have single page application that requires authentication. When user was authenticated then visit some pages or hit reload button in browser it will request an api that provide their auth data. then I have an error like this:
[Vue warn]: Error when evaluating expression "auth.name": TypeError: Cannot read property 'name' of null (found in component: <navbar>)
This error is caused because vue render auth data while the request to api have not finished yet.
Is possible to make vue wait the request api until finish first, before vue render auth data?
just more clearly what going on here. Here is the code:
// main.js
import Vue from 'vue'
import App from './App.vue' // root vue
import store from './vuex/store' // vuex
import router from './router' // my router map
sync(store, router)
router.start(App, '#app')
// end main.js
// App.vue
<template>
<main>
<div class="wrapper">
<router-view></router-view>
</div>
</main>
</template>
<script>
import authService from './services/auth' // import authservice
ready () {
// here is code that should be done first before vue render all authData
auth.getUser((response) => {
self.authData = response
})
},
data () {
return {
authData: null // default is null until the api finish the process
}
}
</script>
// end App.vue
// SomeRouterComponents.vue
<template>
<!-- some content -->
<div>
// always got error: cannot read property 'name' of null
// here I expect to render this after api has been resolved
{{ $root.authData.name }}
</div>
<!-- some content -->
</template>
The problem as you said is that you try to access an object that isn't present, and because of the error, Vue can't render it in the next tick. The solution is to use a simple v-if to check if the data is loaded, this work only for reactive data.
root component
import auth from './services/auth' // import authservice
ready () {
// here is code that should be done first before vue render all authData
auth.getUser((response) => {
self.authData = response
self.dataReady = true
})
},
data () {
return {
authData: null, // default is null until the api finish the process
dataReady: false
}
}
otherComponent
<div v-if="dataReady">
// note that if you use v-show you will get the same error
{{ $root.authData.name }}
</div>
<div v-if="!dataReady">
// or some other loading effect
Loading...
</div>
I used v-if="!dataReady" instead of v-else because it will be deprecated in Vue 2.0
You could use the data transition hook with the waitForDataoption enabled:
<script>
import authService from './services/auth'
export default {
route: {
waitForData: true, // wait until data is loaded
data (transition) {
authService.getUser((response) => {
transition.next({ authData: response }) // set data
})
}
},
data () {
return {
authData: null
}
}
}
</script>
Also, if you don't want to use that option, you could check if the data is loaded by using the $loadingRouteData property.
More details here:
http://router.vuejs.org/en/pipeline/data.html
You could just prevent Vue from trying to access the object property, like this:
{{ $root.authData ? $root.authData.name : null }}
You could even change null for a Loading... message in some cases.