How to look up the element in the HTML in the .vue file that needs to be updated: - vue.js

You can consider me a toddler in the vuejs worlđ
In my app i fetch some posts that are present in firebase database
Each post has a upvote and downvote button just like stack overflow where users can upvote or downvote( it's completely u to them)
Eeverything like votes getting updated to databae and rest all works great*
Here comes the problem
Firebase provides a event listener for listening whenever there is change in each child i.e post (in my case upvotes. Downvotes)
i add this listener to the created() lifecycle hook so that the votes update when there is change in them by other users
here is the simplified code below of my .vue file
<template>
<div>
<div v-for="post in posts" id="post.key" class="container">
<p id="upvotes">{{ post.up}}</p>
<p id="downvotes">{{ post.down }}</p>
</div>
</div>
</template>
<script>
export default{
created:{
const ref = this.$firebase.database().ref();
ref.child("posts").on('child_changed', function(post) {
var upvotes = post.val().up;
var downvotes = post.val().down;
//how to look up the element in the HTML above that needs to be updated:
//if it were plain javascript we would have done something like this
//var postElm = document.getElementById(post.key);
//postElm.getElementById("upvotes").innerHTML = upvotes;
//postElm.getElementById("downvotes").innerHTML = downvotes;
});
}
}
</script>
My issue:
how to look up the upvotes and downvotes element in the HTML above in the template that needs to be updated:
according to the docs we can register a reference ref to an element but there is this note saying:
because the refs themselves are created as a result of the render function, you cannot access them on the initial render - they don’t exist yet! $refs is also non-reactive, therefore you should not attempt to use it in templates for data-binding.
so how can I reference the elements to update them

Update: Here is an example where I have mocked the firebase behavior. posts should be a data item, because you control its contents. computed items are for derived values based on other data or props items. You wouldn't make a computed based on a value external to Vue, because such values are not reactive.
const payloads = [{
val() {
return {
key: 2,
up: 10,
down: 3
};
}
},
{
val() {
return {
key: 1,
up: 3,
down: 10
};
}
}
];
new Vue({
el: '#app',
components: {
postVoter: {
template: '#post-voter-template',
data() {
return {
posts: [{
key: 1,
up: 0,
down: 0
},
{
key: 2,
up: 1,
down: 0
},
{
key: 3,
up: 0,
down: 1
}
]
}
},
created() {
const i = setInterval(() => {
if (payloads.length) {
console.log("Doing");
const post = payloads.shift();
const item = this.posts.find((p) => p.key === post.val().key);
item.up = post.val().up;
item.down = post.val().down;
} else {
console.log("Done");
clearInterval(i);
}
}, 1000);
}
}
}
});
.container {
display: flex;
justify-content: space-between;
width: 10rem;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.2.6/vue.min.js"></script>
<template id="post-voter-template">
<div>
<div v-for="post in posts" class="container">
<p id="upvotes">{{ post.up}}</p>
<p id="downvotes">{{ post.down }}</p>
</div>
</div>
</template>
<post-voter id="app"></post-voter>
You should be finding which data item needs updated, rather than trying to find it in the DOM. You don't say how posts is created or populated, so I'm having to surmise what it looks like. The code should go something like this:
<template>
<div>
<div v-for="post in posts" class="container">
<p id="upvotes">{{ post.up}}</p>
<p id="downvotes">{{ post.down }}</p>
</div>
</div>
</template>
<script>
export default{
created() {
const ref = this.$firebase.database().ref();
ref.child("posts").on('child_changed', (post) => {
const item = this.posts.find((p) => p.key === post.key);
item.up = post.val().up;
item.down = post.val().down;
});
}
}
</script>

Related

How to change a prop value in a generated vue components for single instance or for all instances?

Trying to create a simple blog style page. Every post has a like button, that increments when clicked. I generate 10 of these components with a v-for loop, taking data from a vuex store. However, I'd like there to be a button on the home page that resets all of the like counters.
By googling I seem to find and get working solutions that do either one or the other, not together. Yet to get anything working at all except singular counters.
How can I add a button that resets all the PostEntity counter props? Or how should I restructure it? I've thought about somehow doing in with states.
This is my post component, that gets looped in the main view .vue object:
<template>
<div class="post">
<div class="postheader">
<img :src="profilePic" alt="profilepic" class="profilepic" />
<p>{{ postDate }}</p>
</div>
<div class="postbody">
<img :src="postImage" />
<p>{{ postParagraph }}</p>
</div>
<div class="postfooter">
<!--<img :src="require('#/assets/' +nation.drapeau)"/> -->
<img
:src="require('#/assets/like.png')"
class="likepilt"
#click.prevent="increment"
/>
<p>Number of likes: {{ count }}</p>
</div>
</div>
</template>
<script>
export default {
name: 'PostEntity',
props: {
postDate: String,
postImage: String,
profilePic: String,
postParagraph: String
},
data: function () {
return {
count: 0
};
},
methods: {
increment() {
this.count++;
}
}
};
</script>
This is how I retrieve info from my VueX store:
getters: {
postListStuff: state => {
const postListStuff = state.postList.map(post => {
return {
id: post.id,
img: post.img,
profilepic: post.profilepic,
date: post.date,
paragraph: post.paragraph
};
});
return postListStuff;
}
}
This is how I display the components and generate the posts:
<template>
<HeaderBox title-text="Homepage" />
<div v-for="post in postListStuff" :key="post.id" class="posts">
<PostEntity
:post-date="post.date"
:profile-pic="post.profilepic"
:post-image="post.img"
:post-paragraph="post.paragraph"
></PostEntity>
</div>
<FooterBox />
<HelloWorld />
</template>
<script>
import HelloWorld from './components/HelloWorld.vue';
import HeaderBox from '#/components/Header';
import FooterBox from '#/components/Footer';
import PostEntity from '#/components/Post';
export default {
name: 'App',
components: {
FooterBox,
HeaderBox,
HelloWorld,
PostEntity
},
computed: {
postListStuff() {
return this.$store.getters.postListStuff;
}
}
};
</script>
There are multiple possible ways to go about doing this, but the simplest way I can think of with least amount of code would be:
Add a reset method to the PostEntity component that sets count to 0.
methods: {
increment() {
this.count++;
},
reset() {
this.count = 0;
}
}
Then in the parent component add a ref to the PostEntity components inside the v-for loop, then add a new button with onclick method resetCounters:
<div v-for="post in postListStuff" :key="post.id" class="posts">
<PostEntity
ref="post"
:post-date="post.date"
:profile-pic="post.profilepic"
:post-image="post.img"
:post-paragraph="post.paragraph"
></PostEntity>
</div>
<button #click="resetCounters">Reset</button>
resetCounters will loop through the array of PostEntity refs and call the reset method on each of them.
methods: {
resetCounters() {
this.$refs.post.forEach(p => p.reset());
}
}

How to access a Vue component's data from a script

Here are the simplified html and javascript files of the page. It has a button and component which is a text displays the data of the component. I want the component's data to be changed when I click the button. But how to access the component's data from a script?
index.html
<body>
<div id="app">
<my-component></my-component>
<button id="btn"> change data </button>
</div>
<script src="https://unpkg.com/vue#next"></script>
<script src="./main.js"></script>
</body>
main.js
let app = Vue.createApp({});
app.component('my-component', {
data: function() {
return {
component_data : "foo"
}
},
template: '<p> data = {{ component_data }} </p>'
}
);
app.mount("#app");
document.querySelector("btn").onclick = function() {
// HOW TO CHANGE component_data TO "bar"
}
One possibility is to incorporate the button into the HTML within the component's template. If that's feasible for your app then you can add a function to the component and bind the function to the button's click event.
E.g. (Note this is untested so may have typos)
app.component('my-component', {
data: function() {
return {
component_data : "foo"
}
},
methods: {
changeData() {
this.component_data = "The data changed";
}
},
template: `<p> data = {{ component_data }} </p>
<button #click="changeData">Change data</button>`
}
);
If the button can't be incorporated into my-component then I'd recommend using the Vuex datastore. Vuex is a reactive datastore that can be accessed across the entire application.
You can use component props change data between components.
index.html
<body>
<div id="app">
<my-component :component-data="text"></my-component>
<button #click="handleBtnClick"> change data </button>
</div>
<script src="https://unpkg.com/vue#next"></script>
<script src="./main.js"></script>
</body>
main.js file
let app = Vue.createApp({
data() {
return { text: 'foo' }
},
methods: {
handleBtnClick() {
this.text = 'bar';
}
}
});
app.component('my-component', {
props: {
componentData: {
type: String,
default: 'foo'
}
}
template: '<p> data = {{ componentData }} </p>'
}
);
app.mount("#app");
I think you new in Vuejs. You have to first read Vue documentation
To get the reference of a component outside of it, you can use the template refs
Here is the refactor of the code provided in the above question to access the components data from the script.
<div id="app">
<my-component ref="my_component"></my-component>
<button #click="onBtnClick()"> change data </button>
</div>
let app = Vue.createApp({
methods: {
onBtnClick() {
this.$refs.my_component.component_data = "bar";
}
}
});

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
}
}

Click event on div to get innerText value and $emit with event-bus to another component not working

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" });
}

Using the same component template and updating the API data

I'm building a page that has a embeds a specific twitch stream video. I'm only displaying one video at the top of my page.
Twitch has an embed code that allows you to grab the channel you want to watch and it will display the embedded video and chat. It requires a div id to target the DOM to add the embedded video.
https://dev.twitch.tv/docs/embed/everything/
My problem is when I click on another page, that uses the same template, it doesn't replace the video. Rather, it adds another IFRAME embed video to the id. So every time I click on the page, it just adds another video to the div id.
I'm using the watch function to update other elements of the page. So when I click on another page, using the same template, the data updates correctly. Everything works and updates except for that embed video.
Is there a way to clear out that div id every time I click another another page? I apologize in advance. I've only been learning Vuejs for a couple of weeks now, and it's all rather new to me.
Here is why my template looks like:
<template>
<div class="video heading-title container">
<div class="streamWrapper">
<div class="row">
<div v-for="live in streams" class="col-12 stream-card">
<div class="twitch-vid-wrapper">
<div id="twitch-embed"></div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import appService from '../service.js'
import '../embedTwitch.min.js' // twitch video embed script
export default {
name: 'Video',
data: function() {
return {
id: this.$route.params.id,
streams: []
}
},
created() {
this.getFirstLiveStream()
this.getLiveStreams()
},
watch: {
'$route' (to, from) {
this.id = to.params.id
this.getLiveStreams()
this.getFirstLiveStream()
}
},
methods: {
getLiveStreams(game){
game = this.$route.params.id;
appService.getLiveStreams(game).then(data => {
this.live = data
});
},
getFirstLiveStream(game) {
game = this.$route.params.id;
appService.getFirstLiveStream(game).then(data => {
this.streams = data
let channelName = this.streams[0].channel.display_name
appService.getTwitchStream(channelName)
});
}
}
}
</script>
Here is the method I have in my service:
const appService = {
getFirstLiveStream(game) {
return new Promise((resolve) => {
axios.get('/kraken/streams/?sort=views&stream_type=live&game='+game)
.then((response) => {
// send variables to calc the offset
var total = response.data._total;
var query = this.calculateSingleOffset(game, total)
resolve(query)
})
})
},
getTwitchStream(channel) {
return setTimeout(function(){
new Twitch.Embed('twitch-embed', {
width: '100%',
height: 480,
channel: channel
});
}
, 500);
}
}
Thanks!
As I understood, what you need is how to assign different id for one twitch template inside each instance of the component.
The solution:
add one data property like twitchId
simply uses Date.now() to generate unique id (this method is just one demo, you can use own methods to get one ID).
then bind <div :id="twitchId"> which will embed into twitch video.
Vue.config.productionTip = false
function embedContent(elementId) {
let contents = ['I am a test', 'nothing', 'connecting', 'bla bla ...', 'Puss in boots', 'Snow White']
document.getElementById(elementId).innerHTML = '<p>' + contents[Math.floor(Math.random()*contents.length)] + '</p>'
}
Vue.component('twitch', {
template: `<div class="video">
<h2>{{name}}</h2>
<div :id="twitchId"></div>
</div>`,
props: ['name'],
data() {
return {
twitchId: ''
}
},
created: function () {
this.twitchId = Date.now().toString(16)
},
mounted: function () {
embedContent(this.twitchId)
}
})
app = new Vue({
el: "#app",
data: {
videos: [
'channel 1',
'World Cup for Football',
'StackOverFlow'
]
}
})
.video {
border: 1px solid red;
}
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<div>
<twitch v-for="(item, index) in videos" :name="item" :key="index"></twitch>
</div>
</div>