I am getting an array of objects from firebase and showing them characters component in list format using V-for. Everytime I go to homepage and returning to characters page the list are getting multiplied and showing me duplicate keys.
characters.vue:
<template>
<ul class="characters-list">
<li v-for="allHero in getAllHeros" v-bind:key="allHero.id">
<router-link :to="{ name: 'characterDetail', params: { id: allHero.id } }">
<div class="hero-thumbnail">
<img :src="allHero.imageUrl" :alt="allHero.title" />
</div>
<div class="hero-detail">
<h3>{{ allHero.name }}</h3>
</div>
</router-link>
</li>
</ul>
</template>
import database from "#/firebase/init";
computed: {
...mapGetters({ getAllHeros: "getAllHeros" }),
},
created() {
database
.collection("heros")
.get()
.then(snapshot => {
snapshot.forEach(doc => {
let heros = doc.data();
heros.id = doc.id;
this.$store.dispatch('fetchAllHeros', heros)
});
});
}
VUEX Module -
const state = {
allHeros: []
};
const getters = {
getAllHeros: state => {
return state.allHeros;
}
};
const actions = {
async fetchAllHeros({ commit }, heros) {
commit("setAllHeros", heros);
}
};
const mutations = {
setAllHeros: (state, payload) => {
state.allHeros.push(payload);
}
};
When you route to a new page your Vuex store does not necessarily get reset to its initial state. Therefore every time that component is created you are adding more heros to the vuex store which is resulting in duplicate heros being added.
To prevent this, you can just use some simple logic to check if any heroes have been loaded:
created() {
if(this.getAllHeros.length == 0){
//Get heros from database..
};
}
Related
My template has following:
<ul id="example-1">
<li v-for="item in getMenus" :key="item.id">
{{ item.name }}
</li>
</ul>
methods:{
async getMenus() {
this.$axios.setHeader('Content-Type', 'application/json', ['get'])
this.$axios.setHeader(
'Authorization',
'Bearer ' + this.$store.state.auth.Token
)
const roleId = this.$store.state.auth.role.roleId
const url = `/role/${roleId}/menu`
let data = ''
// eslint-disable-next-line vue/no-async-in-computed-properties
const pal = await this.$axios
.$get(url, JSON.stringify(roleId))
.then(function(resp) {
data = resp.data
})
if (pal) {
// eslint-disable-next-line no-console
console.log('hi')
}
return data
}
}
}
Above mentioned is my code. I checked my api its returing data. If i put directly my data as harcoded value then it works, if I use api then it doesnot work. I looke dinto console also that is also clear. I am new to vue. Any help will be highly appreciated.
You can't use async methods in v-for. Define an array in data section of a component and write results in the array at the end of getMenus function. You should call getMenus at some place in your code (for instance in mounted hook):
<li v-for="item in menuList" :key="item.id">
...
// in a component code
data: {
return {
menuList: []
}
},
mounted () {
// if you don't have any initialization after this call you can call it without await
getMenus()
},
methods:{
async getMenus() {
...
// getting results
const { data: menuList } = await this.$axios
.$get(url, JSON.stringify(roleId))
this.menuList = menuList
}
This happens because inside async getMenus method you are returning data before it is even assigned a value. A better way to resolve this issue would be to set a variable in data options like:
data() {
return {
loading: false,
items: [] // This will hold all the getMenus() data
}
},
and inside getMenus update items array like:
created() {
this.getMenus();
},
methods: {
async getMenus() {
this.loading = true;
// All other logic here...
this.$axios.$get(url, JSON.stringify(roleId))
.then(resp => {
this.loading = false;
this.items = resp.data; // Set the response data here...
})
.catch(error => {
this.loading = false;
console.log(error);
})
}
}
and then update your template like:
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
In case, your async method is going to take some time to finish you can show a loading text or icon so that user know that at least something is happening instead of looking at a blank screen like:
<template v-if="loading">
Loading...
</template>
<template v-else>
<ul id="example-1">
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
</template>
I have a list of divs that include product information which i get from an API call. In another component/view i want to display a single product information when the divs are clicked on.
So what i'm trying to do is retrieve the product id by accessing the event object when clicking on the divs then store that id in a variable (not data property) and then $emit it with the event-bus and then listen for it in my other component and use that id to make the API call to get the information for that single product. I'm not sure if this is the best way of doing what i want to do, but its the only way that comes to mind right now.
However so far i have gotten a few different errors and my component that displays the single product does not render.
This is the component that displays the list of products/divs
<template>
<div>
<div class="pagination">
<button :disabled="disabled" #click.prevent="prev()">
<i class="material-icons">arrow_back</i>
</button>
<span class="page-number">{{ currentPage }}</span>
<button #click.prevent="next()">
<i class="material-icons">arrow_forward</i>
</button>
</div>
<div class="products">
<div
class="product"
#click="getSingleBeer($event)"
v-for="product in products"
:key="product.id"
>
<h2 class="name">{{ product.name }}</h2>
<div class="image">
<img :src="product.image_url" />
</div>
<h3 class="tagline">{{ product.tagline }}</h3>
<h3 class="first-brewed">{{ product.first_brewed }}</h3>
<h3 class="abv">{{ product.abv }}%</h3>
<p class="id">{{ product.id }}</p>
</div>
</div>
</div>
</template>
<script>
import axios from "axios";
import { eventBus } from "../main";
export default {
name: "Products",
data() {
return {
products: [],
currentPage: 1,
searchVal: ""
};
},
created() {
this.getBeers();
eventBus.$on("keyword", val => {
this.searchVal = val;
this.getBeersForSearch();
});
},
computed: {
apiUrl() {
return `https://api.punkapi.com/v2/beers?page=${this.currentPage}&per_page=16`;
},
apiUrlForSearch() {
return `https://api.punkapi.com/v2/beers?page=${this.currentPage}&per_page=12&beer_name=${this.searchVal}`;
},
disabled() {
return this.currentPage <= 1;
},
isFirstPage() {
return this.currentPage === 1;
}
},
methods: {
async getBeers() {
try {
const response = await axios.get(this.apiUrl);
this.products = response.data;
console.log(response);
} catch (error) {
console.log(error);
}
},
async getBeersForSearch() {
try {
this.currentPage = 1;
const response = await axios.get(this.apiUrlForSearch);
this.products = response.data;
console.log(response);
} catch (error) {
console.log(error);
}
},
getSingleBeer($event) {
const id = parseInt($event.target.lastChild.innerText);
eventBus.$emit("beer-id", id);
this.$router.push({ name: "Beer" });
}
}
};
</script>
And this is the component/view that is going to display info for the single selected product.
<template>
<div class="beer-container">
<div class="description">
<h2>{{ beer.description }}</h2>
</div>
<div class="img-name">
<h1>{{ beer.name }}</h1>
<img :src="beer.image_url" alt />
</div>
<div class="ingredients"></div>
<div class="brewer-tips">
<h2>{{ beer.brewers_tips }}</h2>
</div>
</div>
</template>
<script>
import { eventBus } from "../main";
import axios from "axios";
export default {
name: "Beer",
data() {
return {
beerId: null,
beer: []
};
},
created() {
eventBus.$on("beer-id", id => {
this.beerId = id;
this.getBeer();
console.log(this.beer);
});
},
methods: {
async getBeer() {
try {
const response = await axios.get(this.apiUrl);
this.beer = response.data[0];
console.log(response.data[0]);
} catch (error) {
console.log(error + "Eroorrrrrr");
}
}
},
computed: {
apiUrl() {
return `https://api.punkapi.com/v2/beers/${this.beerId}`;
}
}
};
</script>
Some of the errors i had so far:
1-the api call is made 2-3 simultaneously when i observe console logs instead of just once.
GET https://api.punkapi.com/v2/beers/null 400
Error: Request failed with status code 400Eroorrrrrr
GET https://api.punkapi.com/v2/beers/null 400
Error: Request failed with status code 400Eroorrrrrr
GET https://api.punkapi.com/v2/beers/null 400
Error: Request failed with status code 400Eroorrrrrr
2-The first time i click on the div it directs to the new route/component but i dont receive any errors and nothing seems to happen behind the scenes.
3- I have also been getting this error:
[Vue warn]: Error in v-on handler: "TypeError: Cannot read property 'innerText' of null"
And
TypeError: Cannot read property 'innerText' of null
My router.js
import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";
import Beer from "./views/Beer.vue";
Vue.use(Router);
export default new Router({
mode: "history",
base: process.env.BASE_URL,
routes: [
{
path: "/",
name: "home",
component: Home
},
{
path: "/beer",
name: "Beer",
component: Beer
}
]
});
UPDATE: I'm able to pass the data to the next component but when i click on the product divs the first time nothing happens, i only get directed to the next route/component but data does not get passed. And when i go back and click again,(without refreshing the page) the data gets passed but nothing renders on the component.
I believe you can simplify that a lot by changing your #click to be:
#click="getSingleBeer(product.id)"
Which should pass the id for you, so you can just do:
getSingleBeer(beerId) {
eventBus.$emit("beer-id", beerId);
this.$router.push({ name: "Beer" });
}
Just want to say that I am became familiar with vue not so long ago.
Now I am having a problem.
I fetch data from the server.
// store
import PostService from '../services/PostService'
export default {
state: {
posts: [],
},
actions: {
async getPost() {
const response = await PostService.fetchPosts();
console.log(response.data.posts);
this.posts = response.data.posts
}
}
}
There is an array for data and a request to the server.
In response, the data comes.
// vue component
<template>
<section class="posts--wrap">
<div class="posts wrapper">
<h1>Posts</h1>
<div
class="posts__item--wrap"
v-if="this.allPosts.length"
>
<h3>List of posts</h3>
<div
v-for="(post, index) in allPosts"
:key="index"
class="posts__item">
<h3 class="posts__item-title">{{post.title}}</h3>
<p class="posts__item-text">{{post.description}}</p>
</div>
</div>
<div
v-else
class="posts__item-error"
>
<h3>There are no posts ... Lets add one now!</h3>
<router-link tag="div" :to="{name: 'Add'}">
<a>Add new post ;)</a>
</router-link>
</div>
</div>
</section>
</template>
<script>
import { mapState } from 'vuex';
import { mapActions } from 'vuex';
export default {
name: 'PostPage',
data () {
return {
}
},
computed: mapState({
allPosts: state => state.posts.posts
}),
methods: {
...mapActions({
getAllPosts: 'getPost'
})
},
mounted() {
console.log(this.allPosts);
this.getAllPosts();
}
}
</script>
If add something to state.posts, it will be displayed on the page.
But I can't figure out how to get the data from the response into the posts
I ask for help, or hints.
Thank!
UPD
Made changes for which displays the response
response.log
(2) [{…}, {…}]
0: {_id: "5be4bbfaad18f91fbf732d17", title: "Title post 1", description: "esxdfgdfghj"}
1: {_id: "5be490f930fba81a704867f6", title: "2312", description: "312312312"}
length: 2
__proto__: Array(0)
You did wrong in your action getPost, the proper way to change a state through an action is to commit a mutation using action context parameter like this:
...
mutations: {
setPosts(state, posts) {
state.posts = posts;
},
},
actions: {
async getPost(context) {
/* ... */
context.commit('setPosts', response.data.posts);
}
},
...
Read more about Vuex actions.
I am new to Vue and am trying to build a simple movie app, fetching data from an API and rendering the results. I want to have an incremental search feature. I have an input field in my navbar and when the user types, I want to redirect from the dashboard view to the search results view. I am unsure of how to pass the query params from the navbar to the search results view.
Here is my App.vue component
<template>
<div id="app">
<Navbar></Navbar>
<router-view/>
</div>
</template>
<script>
import Navbar from './components/Navbar.vue'
export default {
name: 'App',
components: {
Navbar
},
}
</script>
And here is my navbar component where I have the input field
<template>
<nav class="navbar">
<h1 class="logo" v-on:click="goToHome">Movie App</h1>
<input class="search-input" v-on:keyup="showResults" v-model="query" type="text" placeholder="Search..."/>
</nav>
</template>
<script>
import router from '../router/index'
export default {
data: function () {
return {
query: this.query
}
},
methods: {
goToHome () {
router.push({name: 'Dashboard'})
},
showResults () {
//here on each key press I want to narrow my results in the SearchedMovies component
}
}
}
</script>
If I use router.push to the SearchedMovies component then I am only able to pass the query as a parameter once. I thought about using Vuex to store the query and then access it from the SearchedMovies component, but surely there is a better way of doing it?
I also read about using $emit but since my parent contains all the routes, I'm not sure how to go about this.
You don't need to redirect user anywhere. I've made a small demo to show how one might do it. I used this navbar component as you described and emit an event from it:
const movies = {
data: [
{
id: 0,
title: 'Eraserhead',
},
{
id: 1,
title: 'Erazerhead',
},
{
id: 2,
title: 'Videodrome',
},
{
id: 3,
title: 'Videobrome',
},
{
id: 4,
title: 'Cube',
},
]
};
Vue.component('navbar', {
template: '<input v-model="filter" #input="onInput" placeholder="search">',
data() {
return {
filter: '',
};
},
methods: {
onInput() {
this.$emit('filter', this.filter);
}
}
});
// this is just a request imitation.
// just waiting for a second until we get a response
// from the datasample
function request(title) {
return new Promise((fulfill) => {
toReturn = movies.data.filter(movie => movie.title.toLowerCase().indexOf(title.toLowerCase()) !== -1)
setTimeout(() => fulfill(toReturn), 1000);
});
}
new Vue({
el: '#app',
data: {
movies: undefined,
loading: false,
filter: '',
lastValue: '',
},
methods: {
filterList(payload) {
// a timeout to prevent
// instant request on every input interaction
this.lastValue = payload;
setTimeout(() => this.makeRequest(), 1000);
},
makeRequest() {
if (this.loading) {
return;
}
this.loading = true;
request(this.lastValue).then((response) => {
this.movies = response;
this.loading = false;
});
}
},
mounted() {
this.makeRequest('');
}
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<navbar v-on:filter="filterList"></navbar>
<ul v-if="!loading">
<li v-for="movie in movies" :key="movie.id">{{ movie.title }}</li>
</ul>
<p v-else>Loading...</p>
</div>
Also jsfiddle: https://jsfiddle.net/oniondomes/rsyys3rp/
If you have any problem to understand the code above let me know.
EDIT: Fixed some bugs and added a couple of comments
EDIT2(after the comment below):
Here's what you can do. Every time user inputs something inside a navbar you call a function:
// template
<navbar v-on:input-inside-nav-bar="atInputInsideNavBar"></navbar>
// script
methods: {
atInputInsideNavBar(userInput) {
this.$router.push({
path: '/filtred-items',
params: {
value: userInput
}
})
}
}
Then inside you 'searched movies' page component you can access this value so:
this.$route.params.value // returns userInput from root component
In vuex store I have this mutations which receives msg from one component and is to show/hide prompt message at another component (Like You are logged in propmpt after successful login) :
setPromptMsg: function (state, msg) {
state.showPromptMsg = true;
state.promptMsg = msg;
function sleep (time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
sleep(3000).then(() => {
store.showPromptMsg = false;
state.promptMsg = '';
console.log('show message set to false');
});
},
In the compoenet, I receive showPromptMsg from the store as a computed property:
computed: {
showPromptMsg () {
return this.$store.state.showPromptMsg;
},
promptMsg () {
return this.$store.state.promptMsg;
}
}
The show/hide prompt message in the template:
<div v-show="showPromptMsg">
<div class="text-center" >
<strong> {{promptMsg}} </strong>
</div>
</div>
The problem is that when the prompt is timedout, i.e. showPromptMsg is set to false at the store, the change is not reflected into the component, so the notification box does not disappear.
I'm wondering what is the idiomatic way to resolve this problem?
The code is setting
store.showPromptMsg = false;
I expect you want
state.showPromptMsg = false;
In your NotificationBarComponent.vue template:
<div>
<div class="text-center" >
<strong> {{ message }} </strong>
</div>
</div>
In your NotificationBarComponent.vue component definition add a prop to pass custom message to display and on mounted start the timeout to hide the notification:
export.default {
props: ['message'],
mounted() {
window.setTimeout(() => {
this.$store.commit('handleMessageState', false)
}, 3000);
}
};
in your store create a property to manage the notification display isNotificationBarDisplayed: false
handleMessageState(state, payload) {
state.isNotificationBarDisplayed = payload;
}
anywhere you want to use it:
<notification-bar-component v-show="isNotificationBarDisplayed" message="Some message here"></notification-bar-component>
computed: {
isNotificationBarDisplayed () {
return this.$store.state.isNotificationBarDisplayed;
},
}