Vue 3 - How do I use an injected list in v-for loop? - vue.js

I have an app that, among other things, keeps track of items in lists. The following example, a slight extension of the docs example (using props), works properly.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Vue List Component Test</title>
<script src="https://unpkg.com/vue#3"></script>
</head>
<body>
<div id="app"></div>
<script src="./app.js"></script>
</body>
</html>
app.js (with props)
const MyApp = {
data() {
return {
items: []
}
},
template: `<list-count></list-count><list :itemList="items"></list>`,
methods: {
addItem(i) {
this.items.push(i)
}
},
provide() {
return {
listLength: Vue.computed(() => this.items.length)
}
},
}
const app = Vue.createApp(MyApp)
const ItemListCount = {
inject: ["listLength"],
template: `<p> {{ this.listLength }}</p>`
}
const ItemList = {
props: ["itemList"],
template:`<ul>
<list-item v-for="(item, index) in itemList" :i="item" :idx="index"></list-item>
</ul>`
}
const ItemListItem = {
props: ["i", "idx"],
template: `<li>{{ idx }}, {{ i.message }}</li>`
}
app.component("list", ItemList)
app.component("list-item", ItemListItem)
app.component("list-count", ItemListCount)
const vm = app.mount('#app')
Because the item lists sit in components that are subject to routing, I'd rather use Provide/Inject vs. having to figure out how to pass props down the chain and through the router. The following app.js, which uses provide/inject instead of props for the list, does not work.
app.js (with provide/inject) ... note the removal of the :itemList binding, the addition of the itemList in provide(), and the change in the ItemList component to use inject instead of props.
const MyApp = {
data() {
return {
items: []
}
},
template: `<list-count></list-count><list></list>`,
methods: {
addItem(i) {
this.items.push(i)
}
},
provide() {
return {
listLength: Vue.computed(() => this.items.length),
itemList: Vue.computed(() => this.items)
}
},
}
const app = Vue.createApp(MyApp)
const ItemListCount = {
inject: ["listLength"],
template: `<p> {{ this.listLength }}</p>`
}
const ItemList = {
inject: ["itemList"],
template:`<ul>
<list-item v-for="(item, index) in itemList" :i="item" :idx="index"></list-item>
</ul>`
}
const ItemListItem = {
props: ["i", "idx"],
template: `<li>{{ idx }}, {{ i.message }}</li>`
}
app.component("list", ItemList)
app.component("list-item", ItemListItem)
app.component("list-count", ItemListCount)
const vm = app.mount('#app')
The above raises the following error in console:
Uncaught TypeError: i is undefined
I assume the error is because the v-for loop isn't working properly with the inject: ["itemList"] whereas it seems to be working fine with a props: ["itemList"]. I can't find any relevant docs that would explain why this is the case. How do I fix the provide/inject version?

It looks like you have some unnecessary stuff inside your provide function.
This should do the job:
provide() {
return {
itemList: this.items
};
}
See this working codepen example
Update
Also, I'm assuming that you're using this code as an example of some more complex operation that actually justifies the use of provide/inject. Otherwise it's an overkill for a list component, and you should simplify it like this
const MyApp = {
data() {
return {
items: []
};
},
template: `
<button #click="addItem(Math.floor(Math.random() * 100))">Add Item</button>
<list :items="items"></list>`,
methods: {
addItem(i) {
this.items.push(i);
}
}
};
const app = Vue.createApp(MyApp);
const ItemList = {
props: { items: Array },
template: `
<p>{{ items.length }}</p>
<ul>
<li v-for="(item, index) in items">{{ index + ', ' + item }}</li>
</ul>`
};
app.component("list", ItemList);
app.mount("#app");

Related

VUE.JS 3 Changing boolean value of one sibling component from another

I have two components - component A and component B that are siblings.
I need to change the boolean value inside of Component-A from the Watcher in Component-B.
Component A code:
<template>
<div></div>
</template>
<script>
export default {
data() {
return {
editIsClicked: false,
}
}
}
</script>
Component B code:
<template>
<v-pagination
v-model="currentPage"
:length="lastPage"
:total-visible="8"
></v-pagination>
</template>
<script>
export default {
props: ["store", "collection"],
watch: {
currentPage(newVal) {
this.paginatePage(newVal);
// NEED TO TOGGLE VALUE HERE - when i switch between pages
},
},
},
};
</script>
The Vue Documentation proposes communicating between Vue Components using props and events in the following way
*--------- Vue Component -------*
some data => | -> props -> logic -> event -> | => other components
*-------------------------------*
It's also important to understand how v-model works with components in Vue v3 (Component v-model).
const { createApp } = Vue;
const myComponent = {
props: ['modelValue'],
emits: ['update:modelValue'],
data() {
return {
childValue: this.modelValue
}
},
watch: {
childValue(newVal) {
this.$emit('update:modelValue', newVal)
}
},
template: '<label>Child Value:</label> {{childValue}} <input type="checkbox" v-model="childValue" />'
}
const App = {
components: {
myComponent
},
data() {
return {
parentValue: false
}
}
}
const app = createApp(App)
app.mount('#app')
<div id="app">
Parent Value: {{parentValue}}<br />
<my-component v-model="parentValue"/>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
I have made a new playground. Hope it helps you now to understand the logic.
You can store data in the main Vue App instance or use a Pinia store for it.
But I would suggest you to start without Pinia to make your app simpler. Using Pinia will make your App much more complicated and your knowledge of Vue seems to be not solid enough for that.
const { createApp } = Vue;
const myComponentA = {
props: ['editIsClicked', 'currentPage'],
template: '#my-component-a'
}
const myComponentB = {
emits: ['editIsClicked'],
data() {
return {
currentPage: 1,
}
},
watch: {
currentPage(newVal) {
this.$emit('editIsClicked', newVal)
}
},
template: '#my-component-b'
}
const App = {
components: {
myComponentA, myComponentB
},
data() {
return {
editIsClicked: false,
currentPage: 1
}
},
methods: {
setEditIsClicked(val) {
this.editIsClicked = true;
this.currentPage = val;
}
}
}
const app = createApp(App)
app.mount('#app')
#app { line-height: 2; }
.comp-a { background-color: #f8f9e0; }
.comp-b { background-color: #d9eba7; }
<div id="app">
<my-component-a :edit-is-clicked="editIsClicked" :current-page="currentPage"></my-component-a>
<my-component-b #edit-is-clicked="setEditIsClicked"></my-component-b>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<script type="text/x-template" id="my-component-a">
<div class="comp-a">
My Component A: <br />editIsClicked: <b>{{editIsClicked}}</b><br/>
currentPage: <b>{{currentPage}}</b><br/>
</div>
</script>
<script type="text/x-template" id="my-component-b">
<div class="comp-b">
My Component B: <br />
<label>CurrentPage:</label> <input type="number" v-model="currentPage" />
</div>
</script>

Why Vue doesn't refresh list using props?

On my App, on mounted() method, I call an API, which give to me a JSON with a list of items; than, I update the prop I've set in my target Homepage component:
Homepage.pages = resJSON.data.pages;
Here's the App code:
<template>
<div id="app">
<Homepage title="PWA Test"/>
</div>
</template>
<script>
import Homepage from './components/Homepage.vue'
export default {
name: 'App',
components: {
Homepage
},
mounted() {
let endpoint = "http://localhost:5000/api/graphql?query={pages(orderBy:{contentItemId:asc}){contentItemId,createdUtc,author,displayText,description%20}}";
fetch(endpoint, {
method:'GET',
headers: {
'Accept':'application/json',
'Content-Type':'application/json'
}
})
.then((response) => {
// check for HTTP failure
if (!response.ok) {
throw new Error("HTTP status " + response.status);
}
// read and parse the JSON
return response.json();
})
.then((res) => {
Homepage.pages = res.data.pages;
})
.catch((error) => {
console.log(error);
});
}
}
</script>
<style>
</style>
Here's the Homepage component:
<template>
<div id="homepage">
<h1>{{ title }}</h1>
<ul>
<li v-for="page in pages" :key="page.description">#{{ page.description }}</li>
</ul>
</div>
</template>
<script>
export default {
name: 'Homepage',
props: {
title: String,
pages: []
}
}
</script>
<style scoped>
</style>
But the ul doesn't update after receiving the JSON and updating the props pages. Where's my error?
you need to get the response.json(); in a data property of the App and then pass it down to the Homepage component. So your code should you look like this,
App code:
<template>
<div id="app">
//binding page into data property
<Homepage title="PWA Test" :pages="pages"/>
</div>
</template>
<script>
import Homepage from './components/Homepage.vue'
export default {
name: 'App',
data: function () {
return {
//data propety
pages : []
}
},
components: {
Homepage
},
mounted() {
let endpoint = "http://localhost:5000/api/graphql?query={pages(orderBy:{contentItemId:asc}){contentItemId,createdUtc,author,displayText,description%20}}";
fetch(endpoint, {
method:'GET',
headers: {
'Accept':'application/json',
'Content-Type':'application/json'
}
})
.then((response) => {
if (!response.ok) {
throw new Error("HTTP status " + response.status);
}
// assign the result to the data property
this.page = response.json();
})
.then((res) => {
Homepage.pages = res.data.pages;
})
.catch((error) => {
console.log(error);
});
}
}
</script>
Do you pass the props in a template after this.pages = res.data.pages?
<Homepage :pages="pages" />
I think there are some mistakes that you have done in your code, if you want change update prop value then you have to initialized your props values in script.
<template>
<div id="homepage">
<h1>{{ title }}</h1>
<ul>
<li v-for="page in currentPages" :key="page.description">#{{ page.description }}
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'Homepage',
props: {
title: String,
pages: []
}
data: function () {
return {
currentPages: this.pages
}
}
}
</script>
I hope this will help you to solve your issue- thanks!

How do I render data from Firebase in vue at page load

I am trying to load data from firebase at page load.
The console logs correctly as follows: {-Ltl2osulqmFnKIRoT5Q: {…}, -LtnKKxEWkEH7DbV7VB-: {…}}.
I can't however get the data rendered. fbData only shows []
This is what I have: (I know that I have to use a v-for loop for rendering, but below is only to see if anything is rendered at all, which in my case isn't)
<template>
<div id="main">
<div id="cardFront">{{fbData}}</div>
</div>
</template>
<script>
module.exports = {
data () {
return {
fbData:[],
}
},
created() {
var ref = firebase.database().ref("Users/MK01111000/cards")
ref.once("value")
.then(function(snapshot) {
this.fbData = snapshot.val()
console.log(this.fbData)
})
}
}
What I am looking for is a way to render my data at page load.
<div id="main">
<div id="cardFront">{{preRenderedData}}</div>
</div>
</template>
<script>
module.exports = {
data () {
return {
preRenderedData: null,
fbData:[],
}
},
created() {
var ref = firebase.database().ref("Users/MK01111000/cards")
ref.once("value")
.then(function(snapshot) {
this.fbData = snapshot.val()
})
},
beforeMount() {
this.preRenderedData = this.fbData
}
}

Fetch data in component on initiation using parameters from Vuex store

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

Vue component data not updating from props

I'm building a SPA with a scroll navigation being populated with menu items based on section components.
In my Home.vue I'm importing the scrollNav and the sections like this:
<template>
<div class="front-page">
<scroll-nav v-if="scrollNavShown" #select="changeSection" :active-section="activeItem" :items="sections"></scroll-nav>
<fp-sections #loaded="buildNav" :active="activeItem"></fp-sections>
</div>
</template>
<script>
import scrollNav from '.././components/scrollNav.vue'
import fpSections from './fpSections.vue'
export default {
data() {
return {
scrollNavShown: true,
activeItem: 'sectionOne',
scrollPosition: 0,
sections: []
}
},
methods: {
buildNav(sections) {
this.sections = sections;
console.log(this.sections)
},
changeSection(e) {
this.activeItem = e
},
},
components: {
scrollNav,
fpSections
}
}
</script>
this.sections is initially empty, since I'm populating this array with data from the individual sections in fpSections.vue:
<template>
<div class="fp-sections">
<keep-alive>
<transition
#enter="enter"
#leave="leave"
:css="false"
>
<component :is="activeSection"></component>
</transition>
</keep-alive>
</div>
</template>
<script>
import sectionOne from './sections/sectionOne.vue'
import sectionTwo from './sections/sectionTwo.vue'
import sectionThree from './sections/sectionThree.vue'
export default {
components: {
sectionOne,
sectionTwo,
sectionThree
},
props: {
active: String
},
data() {
return {
activeSection: this.active,
sections: []
}
},
mounted() {
this.buildNav();
},
methods: {
buildNav() {
let _components = this.$options.components;
for(let prop in _components) {
if(!_components[prop].hasOwnProperty('data')) continue;
this.sections.push({
title: _components[prop].data().title,
name: _components[prop].data().name
})
}
this.$emit('loaded', this.sections)
},
enter(el) {
twm.to(el, .2, {
autoAlpha : 1
})
},
leave(el, done) {
twm.to(el, .2, {
autoAlpha : 0
})
}
}
}
</script>
The buildNav method loops through the individual components' data and pushes it to a scoped this.sections array which are then emitted back to Home.vue
Back in Home.vue this.sections is populated with the data emitted from fpSections.vue and passed back to it as a prop.
When I inspect with Vue devtools the props are passed down correctly but the data does not update.
What am I missing here? The data should react to props when it is updated in the parent right?
:active="activeItem"
this is calld "dynamic prop" not dynamic data. You set in once "onInit".
For reactivity you can do
computed:{
activeSection(){ return this.active;}
}
or
watch: {
active(){
//do something
}
}
You could use the .sync modifier and then you need to emit the update, see my example on how it would work:
Vue.component('button-counter', {
template: '<button v-on:click="counter += 1">{{ counter }}</button>',
props: ['counter'],
watch: {
counter: function(){
this.$emit('update:counter',this.counter)
}
},
})
new Vue({
el: '#counter-sync-example',
data: {
foo: 0,
bar: 0
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.2/vue.min.js"></script>
<div id="counter-sync-example">
<p>foo {{ foo }} <button-counter :counter="foo"></button-counter> (no sync)</p>
<p>bar {{ bar }} <button-counter :counter.sync="bar"></button-counter> (.sync)</p>
</div>