Cannot read properties of null (reading 'find') - vue.js

Hello I am a beginner in JavaScript and Vue.js but trying to make some apps to understand how it works.
I am following this article to create a sample application.
https://morioh.com/p/39a413935071
I could fetch data from the API yet there are some errors in the console in the next step.
Here is my code and my console error.
Could you please explain why I get this error and how to fix it?
Index.html
<div class="container" id="app">
<h3 class="text-center">VueNews</h3>
<div class="row" v-for="posts in processedPosts">
<div class="columns large-3 medium-6" v-for="post in posts">
<div class="card">
<div class="card-divider">
{{ post.title }}
</div>
<a :href="post.url" target="_blank"><img :src="post.image_url"></a>
<div class="card-section">
<p>{{ post.abstract }}</p>
</div>
</div>
</div>
</div>
</div>
main.js
const NYTBaseURL = 'https://api.nytimes.com/svc/topstories/v2/';
const ApiKey = 'MyApiKey';
function buildUrl(url) {
return NYTBaseURL + url + '.json?api-key=' + ApiKey
}
const app = new Vue({
el: '#app',
data: {
results: []
},
mounted() {
this.getPosts('home')
},
methods: {
getPosts(section) {
let url = buildUrl(section)
axios.get(url).then((response) => {
this.results = response.data.results
}).catch(error => {
console.log(error)
})
}
},
computed: {
processedPosts() {
let posts = this.results
// Add image_url_attribute
posts.map(post => {
let imgObj = post.multimedia.find(media => media.format === 'superJumbo')
post.image_url = imgObj ? imgObj.url: 'http://placehold.it/300x200?text=N/A'
})
// Put Array into Chunks
let i, j, chunkedArray = [], chunk = 4
for (i = 0, j = 0; i< posts.length; i += chunk, j++) {
chunkedArray[j] = posts.slice(i, i+chunk)
}
return chunkedArray
}
}
})
Console

The problem is not all posts have a multimedia array. Some have multimedia: null, and null doesn't have a find method. Only arrays (and other iterables) do.
In short, you might want to replace
let imgObj = post.multimedia.find(media => media.format === 'superJumbo')
with
let imgObj = (post.multimedia || [])
.find(media => media.format === 'superJumbo')
If post.multimedia is null, it will search in an empty array, which has a .find method, so your app doesn't break.

Related

Nuxt.js Hackernews API update posts without loading page every minute

I have a nuxt.js project: https://github.com/AzizxonZufarov/newsnuxt2
I need to update posts from API every minute without loading the page:
https://github.com/AzizxonZufarov/newsnuxt2/blob/main/pages/index.vue
How can I do that?
Please help to end the code, I have already written some code for this functionality.
Also I have this button for Force updating. It doesn't work too. It adds posts to previous posts. It is not what I want I need to force update posts when I click it.
This is what I have so far
<template>
<div>
<button class="btn" #click="refresh">Force update</button>
<div class="grid grid-cols-4 gap-5">
<div v-for="s in stories" :key="s">
<StoryCard :story="s" />
</div>
</div>
</div>
</template>
<script>
definePageMeta({
layout: 'stories',
})
export default {
data() {
return {
err: '',
stories: [],
}
},
mounted() {
this.reNew()
},
created() {
/* setInterval(() => {
alert()
stories = []
this.reNew()
}, 60000) */
},
methods: {
refresh() {
stories = []
this.reNew()
},
async reNew() {
await $fetch(
'https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty'
).then((response) => {
const results = response.slice(0, 10)
results.forEach((id) => {
$fetch(
'https://hacker-news.firebaseio.com/v0/item/' +
id +
'.json?print=pretty'
)
.then((response) => {
this.stories.push(response)
})
.catch((err) => {
this.err = err
})
})
})
},
},
}
</script>
<style scoped>
.router-link-exact-active {
color: #12b488;
}
</style>
This is how you efficiently use Nuxt3 with the useLazyAsyncData hook and a setInterval of 60s to fetch the data periodically. On top of using async/await rather than .then.
The refreshData function is also a manual refresh of the data if you need to fetch it again.
We're using useIntervalFn, so please do not forget to install #vueuse/core.
<template>
<div>
<button class="btn" #click="refreshData">Fetch the data manually</button>
<p v-if="error">An error happened: {{ error }}</p>
<div v-else-if="stories" class="grid grid-cols-4 gap-5">
<div v-for="s in stories" :key="s.id">
<p>{{ s.id }}: {{ s.title }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { useIntervalFn } from '#vueuse/core' // VueUse helper, install it
const stories = ref(null)
const { pending, data: fetchedStories, error, refresh } = useLazyAsyncData('topstories', () => $fetch('https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty'))
useIntervalFn(() => {
console.log('refreshing the data again')
refresh() // will call the 'topstories' endpoint, just above
}, 60000) // every 60 000 milliseconds
const responseSubset = computed(() => {
return fetchedStories.value?.slice(0, 10) // optional chaining important here
})
watch(responseSubset, async (newV) => {
if (newV.length) { // not mandatory but in case responseSubset goes null again
stories.value = await Promise.all(responseSubset.value.map(async (id) => await $fetch(`https://hacker-news.firebaseio.com/v0/item/${id}.json?print=pretty`)))
}
})
function refreshData() { refreshNuxtData('topstories') }
</script>

How can I resolve this error when calling v-for?

basically my error is that when I run npm run build on the project it points out that it can't find my manuals list
**npm error:
npm run build
vue-tsc --noEmit && vite build
src/views/ManuaisView.vue:11:80 - error TS2304: Cannot find name 'manuais'.
11 **
Found 1 error.
Code:
<div class="flex flex-wrap -mx-4">
<div class="w-full md:w-1/2 lg:w-1/3 px-4" v-for="(manual, idx) in manuais" :key="manual.key">
<div class="h-full p-8 text-center hover:bg-white rounded-md hover:shadow-xl transition duration-200">
<div class="inline-flex h-16 w-16 mb-6 mx-auto items-center justify-center text-white bg-vermelho rounded-lg">
<i class="far fa-file-pdf fa-2x"></i>
</div>
<h3 class="mb-4 text-xl md:text-2xl leading-tight font-bold">{{manual.nome}}</h3>
<p>{{manual.descritivo}}</p>
<p>{{manual.link}}</p>
</div>
</div>
</div>
</div>
</section>
</template>
<script lang="ts">
import { meudb } from '../db';
var post:any = [];
var items: any;
const postRef = meudb.ref('********').once('value', (snapshot) => {
const documents = snapshot.val();
snapshot.forEach(d => {
post.push({nome:d.val().nome, descritivo:d.val().descritivo, link:d.val().nivel})
});
items = documents
console.log(post);
// do something with documents
});
console.log(postRef);
export default {
data() {
return {
manuais: post,
// manuais: [],
}
},
firebase: {
manuais: {
source: meudb.ref('********'),
asObject: true
}
}
}
</script>
If you want to update the manuais value you should set it to an empty array in its definition (in the data) and update its value in the mounted lifecycle (check documentation here
Example :
export default {
data: () => {
return {
manuais: [],
}
},
mounted() {
var post: any = [];
var items: any;
const postRef = meudb.ref('********').once('value', (snapshot) => {
const documents = snapshot.val();
snapshot.forEach(d => {
post.push({
nome: d.val().nome,
descritivo: d.val().descritivo,
link: d.val().nivel
})
});
items = documents
// do something with documents
});
this.manuais = post; // <-- here updating the value
}
}
Working snippet
Here is a little snippet for you to better undestand how works v for.
In your case, if you apply this logic, your code might work
new Vue({
el: '#app',
data: () => ({
manuais: []
}),
mounted() {
// get data from dataBase here
this.manuais = [{
name: 'foo'
}, {
name: 'bar'
}]
}
})
<script src="https://unpkg.com/vue#2.x/dist/vue.js"></script>
<div id="app">
<div v-for="manual in manuals">{{ manual.name }}</div>
</div>

How to Add javascript paggination code in Vue js component

I'm trying to add pagination code in the Vue component and I've tried to add the code in the mounted hook to call the function but it doesn't work. I want to load the code after component loaded completely.Also, jQuery code doesn't load in Vue component. Do I need to change my code to pure javascript for that. Can you guide me how to fix the issue?
// Create a root instance for each block
var vueElements = document.getElementsByClassName('search-bento-block');
var count = vueElements.length;
const store = new Vuex.Store({
state: {
query: drupalSettings.bento.query ? drupalSettings.bento.query : '',
bentoComponents: []
},
mutations: {
add (state, payload) {
state.bentoComponents.push(payload)
}
},
getters: {
getComponents: state => {
return state.bentoComponents
}
}
})
// Loop through each block
for (var i = 0; i < count; i++) {
Vue.component('results', {
template: `
<div v-if="results && results.length > 0">
<div v-for="result in results">
<div class="search-result-item">
<div class="image-holder">
<img src="https://images.unsplash.com/photo-1517836477839-7072aaa8b121?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80">
</div>
<div class="container-content">
<a v-bind:href="result.url">
<h3 v-html="result.title"></h3>
</a>
<p>Subjects: <span v-html="result.subjects"></span></p>
</div>
</div>
</div>
</div>
<div v-else>
<p>No results found.</p>
</div>
`,
props: ['results'],
})
new Vue({
el: vueElements[i],
store,
data: {
message: 'Hello There!',
results: [],
total: 0,
bentoSettings: [],
},
methods: {
addComponentToStore: function (type) {
this.$store.commit('add', type);
console.log("test");
console.log(this.results.length);
}
},
mounted: function() {
// console.log(this.$route.query.bentoq);
const id = this.$el.id;
this.bentoSettings = drupalSettings.pdb.configuration[id];
var bentoConfig = drupalSettings.pdb.configuration[id].clients[this.bentoSettings.bento_type] ? drupalSettings.pdb.configuration[id].clients[this.bentoSettings.bento_type].settings : [];
axios
.get('/api/search/' + this.bentoSettings.bento_type, {
params: {
query: this.$store.state.query,
plugin_id: this.bentoSettings.bento_type,
bento_limit: this.bentoSettings.bento_limit,
bento_config: bentoConfig,
}
})
.then(response => {
console.log(response);
this.results = response.data.records;
this.total = response.data.total;
this.addComponentToStore({
title: this.bentoSettings.example_field,
count: this.total
});
})
.catch(error => {
console.log(error.response);
})
}
});
}
// I'm trying to call following function in Vue component.
function baseThemePagination1() {
//Pagination
pageSize = 3;
var pageCount = $('.line-content').length / pageSize;
for (var i = 0; i < pageCount; i++) {
$('#pagin').append('<li><a href=\'#\'>' + (i + 1) + '</a></li> ');
}
$('#pagin li').first().find('a').addClass('current')
showPage = function(page) {
$('.line-content').hide();
$('.line-content').each(function(n) {
if (n >= pageSize * (page - 1) && n < pageSize * page)
$(this).show();
});
}
showPage(1);
$('#pagin li a').click(function() {
$('#pagin li a').removeClass('current');
$(this).addClass('current');
showPage(parseInt($(this).text()))
});
}
What you are trying to do is not the recommended way to use vue, direct DOM manipulation is one of the things that vue is made to avoid (although can be done). The Vue way would be to bind the value you want to a variable with v-model assuming it is an input and then create your pagination based on that.
If you insist on DOM manipulation then try ref="line-content" and then call it like so:
this.refs.line-content.
In terms of reacting to a page change click simply use a method in your methods section there is no reason to use jQuery for that.
See here for a simple explanation:
https://medium.com/#denny.headrick/pagination-in-vue-js-4bfce47e573b

How does vuejs react to component data updated asynchronously

I am very new with vuejs and recently started to try to replace some old jquery code that I have and make it reactive with vuejs. The thing is I have a component that gets information from a nodejs server via socket.io asynchronously.
When I get the data and update my component's data I see the changes when I console log it but it does not change the DOM the way I want it to do.
What is the proper way to grab data asynchronously and use it inside a component? I post some parts of my code so you can see it. I will appreciate any advice you can give me. Thanks in advance!
Vue.component('chat', {
data() {
return {
chat: null,
commands: [],
chatOpened: false,
}
},
props: [
'io',
'messages',
'channels',
'connectChat',
'roomChat',
'user',
'userId',
'soundRoute',
],
methods: {
openChat() {
this.chatOpened = true;
},
closeChat() {
this.chatOpened = false;
},
},
created() {
this.chat = this.$io.connect(this.connectChat);
this.commands.push('clear');
let self = this;
$.each(this.channels, function(index, value) {
self.chat.emit('join', {room: index, user: self.user, userId: self.userId}, function(err, cb) {
if (!err) {
users = cb.users;
messages = cb.messages;
if (messages.length > 0) {
self.channels[index].loaded = true;
}
//some more code
}
});
});
console.log(this.channels);
},
template: `
<div>
<div id="container-chat-open-button" #click="openChat" :class="{hide : chatOpened}">
<div>+90</div>
<i class="fas fa-comment-alt"></i>
</div>
<div id="container-chat" class="chat__container" :class="{open : chatOpened}">
<div id="container-chat-close-button" #click="closeChat">
<span>
<div>
<i class="fas fa-comment-alt"></i>
#{{ messages.chat_lobby_icon_title }}
</div>
<i class="icon-arrowdown"></i>
</span>
</div>
<div id="alert-chat" class="chat__container-notifications animated flash"></div>
<div class="row">
<ul>
<li v-for="channel in channels" v-show="channel.loaded === true">Channel loaded</li>
</ul>
</div>
</div>
</div>
`
});
I would expect to see the list of channels with messsages but instead I don't see the list even thought I see my channels with the loaded attribute set to true (by default they all have this attribute set to false).
My guess is that it's this part that is not working as expected.
if (messages.length > 0) {
self.channels[index].loaded = true;
}
The reactive way of doing this is by setting the full object again.
Vue.set(self.channels, index, {
...self.channels[index],
loaded: true
}
EDIT 1:
this.channels.forEach((channel) => {
this.chat.emit('join', {room: index, user: self.user, userId: self.userId}, (err, cb) => {
if (!err) {
users = cb.users;
messages = cb.messages;
if (messages.length > 0) {
Vue.set(self.channels, index, {
...self.channels[index],
loaded: true
}
}
//some more code
}
});
})
You'll need to add support for the rest-spread-operator using babel.

axios call in a method with vuejs and nuxt

For each country in my list of countries i need to make an api call with axios to get another value, here is y component :
<template>
<div>
<div v-for="(country, i) in countries" :key="i">
<div>{{ county[i.id].count }}</div>
</div>
</div>
</template>
In my script i call my method matchCount on mounted and store the value in my county data object :
<script>
export default {
props: {
countries: {
type: Array,
required: true
}
},
data() {
return {
county = {}
};
},
mounted() {
this.matchCount();
},
methods: {
matchCount() {
var paysCount = this.pays;
paysCount.forEach(item => {
this.$axios
.get(
`https://api.com/federation/}/${item.id}/`
)
.then(response => {
this.county[item.id] = {};
this.county[item.id].count = response.data.length.toString();
});
});
}
}
};
</script>
I get this error "TypeError: Cannot read property 'count' of undefined", how should i call this method ?
You will find useful using the following syntax in your HTML templates {{variable[key] && variable[key].value}}.
In your particular case it would be:
<template>
<div>
<div v-for="(country, i) in countries" :key="i">
<div>{{ county[i.id] && county[i.id].count }}</div>
</div>
</div>
</template>
What it does, is essentially verifying if the key i.id exists in county array. If not, it will not throw error about missing objects / keys.
You can use this syntax when using objects too as following:
<div v-text="house.dog && house.dog.name" ></div>
If dog is in the house object then it will show dog's name.
Edit:
Add this.$forceUpdate(); to the function:
matchCount() {
var paysCount = this.pays;
paysCount.forEach(item => {
this.$axios
.get(
`https://api.com/federation/}/${item.id}/`
)
.then(response => {
this.county[item.id] = {};
this.county[item.id].count = response.data.length.toString();
this.$forceUpdate();
});
});
}
county[item.id].count is set asynchronously, it might not be available when you render the component. You can add a safe check:
<template>
<div>
<div v-for="(country, i) in countries" :key="i">
<div v-if="county[i.id]">{{ county[i.id].count }}</div>
<div v-else>Loading...</div>
</div>
</div>
</template>
and it seems that you have reactivity problem:
this.$axios
.get(
`https://api.com/federation/}/${item.id}/`
)
.then(response => {
this.$set(this.county, item.id, {count: response.data.length.toString())
});