Unable to Define Variable in Vue - vue.js

I'm just starting to use VueJS & Tailwind, having never really used anything related to npm before.
I have the below code, making use of Tailwind & Headless UI which through debugging, I know I'm like 99% of the way there... except for the continuous error message
Uncaught ReferenceError: posts is not defined
I know this should be straight forward, but everything I've found either here or with Google hasn't worked. Where am I going wrong?
<template>
<Listbox as="div" v-model="selected">
<ListboxLabel class="">
Country
</ListboxLabel>
<div class="mt-1 relative">
<ListboxButton class="">
<span class="">
<img :src="selected.flag" alt="" class="" />
<span class="">{{ selected.name }}</span>
</span>
<span class="">
<SelectorIcon class="" aria-hidden="true" />
</span>
</ListboxButton>
<transition leave-active-class="" leave-from-class="opacity-100" leave-to-class="opacity-0">
<ListboxOptions class="">
<ListboxOption as="template" v-for="country in posts" :key="country" :value="country" v-slot="{ active, selected }">
<li :class="">
<div class="">
<img :src="country.flag" alt="" class="" />
<span :class="[selected ? 'font-semibold' : 'font-normal', 'ml-3 block truncate']">
{{ country.latin }}
</span>
</div>
<span v-if="selected" :class="">
<CheckIcon class="" aria-hidden="true" />
</span>
</li>
</ListboxOption>
</ListboxOptions>
</transition>
</div>
</Listbox>
</template>
<script>
import { ref } from 'vue'
import { Listbox, ListboxButton, ListboxLabel, ListboxOption, ListboxOptions } from '#headlessui/vue'
import { CheckIcon, SelectorIcon } from '#heroicons/vue/solid'
import axios from 'axios'
export default {
data() {
return {
response: null,
posts: undefined,
};
},
components: {
Listbox,
ListboxButton,
ListboxLabel,
ListboxOption,
ListboxOptions,
CheckIcon,
SelectorIcon,
},
mounted: function() {
axios.get('http://localhost')
.then(response => {
this.posts = response.data;
});
},
setup() {
const selected = ref(posts[30])
return {
selected,
}
},
}
</script>
The offending line is const selected = ref(posts[30]) which I know I need to somehow define posts, but I don't get how?

CAUSE OF YOUR ERROR:
You are trying to access an array element before the array is populated. Thus the undefined error.
EXPLANATION
You are using a mix of composition api and options api. Stick to one.
I am writing this answer assuming you will pick the composition api.
Follow the comments in the below snippet;
<script>
// IMPORT ONMOUNTED HOOK
import { ref, onMounted } from 'vue'
import { Listbox, ListboxButton, ListboxLabel, ListboxOption, ListboxOptions } from '#headlessui/vue'
import { CheckIcon, SelectorIcon } from '#heroicons/vue/solid'
import axios from 'axios'
export default {
// YOU DO NOT NEED TO DEFINE THE DATA PROPERTY WHEN USING COMPOSITION API
/*data() {
return {
response: null,
posts: undefined,
};
},*/
components: {
Listbox,
ListboxButton,
ListboxLabel,
ListboxOption,
ListboxOptions,
CheckIcon,
SelectorIcon,
},
// YOU DO NOT NEED THESE LIFE CYCLE HOOKS; COMPOSITION API PROVIDES ITS OWN LIFECYCLE HOOKS
/*mounted: function() {
axios.get('http://localhost')
.then(response => {
this.posts = response.data;
});
},*/
setup() {
// YOU ARE TRYING TO ACCESS AN ELEMENT BEFORE THE ARRAY IS POPULATED; THUS THE ERROR
//const selected = ref(posts[30])
const posts = ref(undefined);
const selected = ref(undefined);
onMounted(()=>{
// CALL THE AXIOS METHOD FROM WITHIN THE LIFECYCLE HOOK AND HANDLE THE PROMISE LIKE A BOSS
axios.get('http://localhost')
.then((res) => {
selected.value = res[30];
});
});
return {
selected,
}
},
}
</script>
According to your comment; you should first check if the “selected != null” before using ‘selected’ inside the template. You can use a shorthand version like this
<img :src=“selected?.flag” />

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 can I emit from component and listen from another one?

I have in Layout.vue to components one TheSidebar second TheHeader, there is a button in TheHeader to open the sidebar in TheSidebarcomponent.
I need to when I click the button in header open the sidebar:
My try:
in TheHeader:
methods: {
openSidebar() {
this.$root.$emit("open-sidebar");
},
},
in TheSidebar
data() {
return {
sidebarOpen: false,
};
},
mounted() {
this.$root.$on("open-sidebar", (this.sidebarOpen = true));
},
I'm using VUE 3 so I got this error in console: TypeError: this.$root.$on is not a function so How can communicate ?
you can use something like tiny emitter it works fine and doesn't care about parent child relationship
var emitter = require('tiny-emitter/instance');
emitter.on('open-sidebar', ({isOpen}) => {
//
});
emitter.emit('open-sidebar', {isOpen : true} );
You can only pass props to a direct child component, and
you can only emit an event to a direct parent. But
you can provide and eject from anywhere to anywhere
Per another answer, provide and eject may be your best bet in Vue 3, but I created a simple example of how to implement with props/events. Built with Vue 2 as I haven't worked with 3 yet, but should be usable in Vue 3 as well.
Parent.vue
<template>
<div class="parent">
<div class="row">
<div class="col-md-6">
<h4>Parent</h4>
<hr>
<child-one #show-child-two-event="handleShowChildTwoEvent" />
<hr>
<child-two v-if="showChildTwo" />
</div>
</div>
</div>
</template>
<script>
import ChildOne from './ChildOne.vue'
import ChildTwo from './ChildTwo.vue'
export default {
components: {
ChildOne,
ChildTwo
},
data() {
return {
showChildTwo: false
}
},
methods: {
handleShowChildTwoEvent() {
this.showChildTwo = true;
}
}
}
</script>
ChildOne.vue
<template>
<div class="child-one">
<h4>Child One</h4>
<button class="btn btn-secondary" #click="showChildTwo">Show Child Two</button>
</div>
</template>
<script>
export default {
methods: {
showChildTwo() {
this.$emit('show-child-two-event');
}
}
}
</script>
ChildTwo.vue
<template>
<div class="child-two">
<h4>Child Two</h4>
</div>
</template>

(Vue) I have problems reusing references from a composable function

I hope it is okay that I included my full code. Otherwise it would be difficult to understand my question.
I have made a composable function for my Vue application, which purpose is to fetch a collection of documents from a database.
The composable looks like this:
import { ref, watchEffect } from 'vue'
import { projectFirestore } from '../firebase/config'
const getCollection = (collection, query) => {
const documents = ref(null)
const error = ref(null)
let collectionRef = projectFirestore.collection(collection)
.orderBy('createdAt')
if (query) {
collectionRef = collectionRef.where(...query)
}
const unsub = collectionRef.onSnapshot(snap => {
let results = []
snap.docs.forEach(doc => {
doc.data().createdAt && results.push({ ...doc.data(), id: doc.id })
})
documents.value = results
error.value = null
}, (err) => {
console.log(err.message)
document.value = null
error.value = 'could not fetch data'
})
watchEffect((onInvalidate) =>{
onInvalidate(() => unsub());
});
return {
documents,
error
}
}
export default getCollection
Then I have a component where I store the data from the database
<template>
<div v-for="playlist in playlists" :key="playlist.id">
<div class="single">
<div class="thumbnail">
<img :src="playlist.coverUrl">
</div>
<div class="info">
<h3>{‌{ playlist.title }}</h3>
<p>created by {‌{ playlist.userName }}</p>
</div>
<div class="song-number">
<p>{‌{ playlist.songs.length }} songs</p>
</div>
</div>
</div>
</template>
<script>
export default {
// receiving props
props: ['playlists'],
}
</script>
And finally, I output the data inside the main Home component, where I use the documents and error reference from the composable file.
<template>
<div class="home">
<div v-if="error" class="error">Could not fetch the data</div>
<div v-if="documents">
<ListView :playlists="documents" />
</div>
</div>
</template>
<script>
import ListView from '../components/ListView.vue'
import getCollection from '../composables/getCollection'
export default {
name: 'Home',
components: { ListView },
setup() {
const { error, documents } = getCollection('playlists')
return { error, documents }
}
}
</script>
That is all well and good.
But now I wish to add data from a second collection called "books", and the idea is to use the same composable to fetch the data from that collection as well,
but the problem is that inside the Home component, I cannot use the references twice.
I cannot write:
<template>
<div class="home">
<div v-if="error" class="error">Could not fetch the data</div>
<div v-if="documents">
<ListView :playlists="documents" />
<ListView2 :books="documents" />
</div>
</div>
</template>
export default {
name: 'Home',
components: { ListView, ListView2 },
setup() {
const { error, documents } = getCollection('playlists')
const { error, documents } = getCollection('books')
return { error, documents }
}
}
This will give me an error because I reference documents and error twice.
So what I tried was to nest these inside the components themselves
Example:
<template>
<div v-for="playlist in playlists" :key="playlist.id">
<div class="single">
<div class="thumbnail">
<img :src="playlist.coverUrl">
</div>
<div class="title">
{{ playlist.title }}
</div>
<div class="description">
{{ playlist.description }}
</div>
<div>
<router-link :to="{ name: 'PlaylistDetails', params: { id: playlist.id }}">Edit</router-link>
</div>
</div>
</div>
</template>
<script>
import getCollection from '../composables/getCollection'
export default {
setup() {
const { documents, error } = getCollection('playlists')
return {
documents,
error
}
}
}
</script>
This does not work either.
I will just get a 404 error if I try to view this component.
So what is the correct way of writing this?
Try out to rename the destructed fields like :
const { error : playlistsError, documents : playlists } = getCollection('playlists')
const { error : booksError, documents : books } = getCollection('books')
return { playlistsError, playlists , booksError , books }

Use more than one directive to add data attributes to components

I have two directives which are supposed to add data attributes to components for testing, however, only one of the directives actually gets added. The two components are Bootstrap-Vue's BFormInput and BButton.
I tried removing everything but one of the buttons and the directive is still not added i.e
<b-input-group class="sm-2 mb-2 mt-2">
<b-button
variant="primary"
#click="searchJobs"
class="rounded-0"
v-jobs-search-button-directive="{ id: 'search-button' }"
>
Search
</b-button>
</b-input-group>
wrapper.html() output is:
<b-input-group-stub tag="div" class="sm-2 mb-2 mt-2"><b-button-stub target="_self" event="click" routertag="a" variant="secondary" type="button" tag="button" class="rounded-0">
Search
</b-button-stub></b-input-group-stub>
However, it is added when instead of a button I leave in place the input form i.e.
<b-input-group class="sm-2 mb-2 mt-2">
<b-form-input
v-jobs-search-input-directive="{ id: 'input-keyword' }"
class="mr-2 rounded-0"
placeholder="Enter Search term..."
:value="this.searchConfig.Keyword"
#input="this.updateJobsSearchConfig"
/>
</b-input-group>
wrapper.html() output is:
<b-input-group-stub tag="div" class="sm-2 mb-2 mt-2"><b-form-input-stub value="" placeholder="Enter Search term..." type="text" class="mr-2 rounded-0" data-jobs-search-input-id="input-keyword"></b-form-input>
This is how I add the directives
<template>
<b-input-group class="sm-2 mb-2 mt-2">
<b-form-input
v-jobs-search-input-directive="{ id: 'input-keyword' }"
class="mr-2 rounded-0"
placeholder="Enter Search term..."
:value="this.searchConfig.Keyword"
#input="this.updateJobsSearchConfig"
/>
<b-button
variant="primary"
#click="searchJobs"
class="rounded-0"
v-jobs-search-button-directive="{ id: 'search-button' }"
>
Search
</b-button>
</b-input-group>
</template>
<script>
import { mapActions, mapState } from 'vuex'
import JobService from '#/api-services/job.service'
import JobsSearchInputDirective from '#/directives/components/jobs/JobsSearchInputDirective'
import JobsSearchButtonDirective from '#/directives/components/jobs/JobsSearchButtonDirective'
export default {
name: 'jobs-search',
directives: { JobsSearchInputDirective, JobsSearchButtonDirective },
data () {
return {
jobs: [],
pages: 0
}
},
computed: {
...mapState({
pagedConfig: state => state.jobs.paged,
searchConfig: state => state.jobs.search
})
},
methods: {
// Methods go here
}
}
jobs-search-input-directive is
export default (el, binding) => {
if (process.env.NODE_ENV === 'test') {
Object.keys(binding.value).forEach(value => {
el.setAttribute(`data-jobs-search-input-${value}`, binding.value[value])
})
}
}
jobs-search-button-directive is
export default (el, binding) => {
if (process.env.NODE_ENV === 'test') {
Object.keys(binding.value).forEach(value => {
el.setAttribute(`data-jobs-search-button-${value}`, binding.value[value])
})
}
}
This is the test I run, mounting with shallowMount
it('should call jobsSearch method on search button click event', () => {
wrapper.find('[data-jobs-search-button-id="search-button"]').trigger('click')
expect(searchJobs).toHaveBeenCalled()
})
which comes back with
Error: [vue-test-utils]: find did not return [data-jobs-search-button-id="search-button"], cannot call trigger() on empty Wrapper
However wrapper.find('[data-jobs-search-input-id="input-keyword"]') DOES find the input-form
The two directives are registered in the JobsSearch.vue component and they definitely get rendered if I remove the process.env part
I expect the attribute to be added to both components but it only gets added to the BFormInput when testing. Any help will be greatly appreciated.
I believe that the problem occurs when...
... trying to use a directive...
... on a functional child component...
... with shallowMount.
b-button is a functional component.
I've put together the demo below to illustrate the problem. It mounts the same component in 3 different ways and it only fails in the specific case outlined above.
MyComponent = {
template: `
<div>
<my-normal v-my-directive></my-normal>
<my-functional v-my-directive></my-functional>
</div>
`,
components: {
MyNormal: {
render: h => h('span', 'Normal')
},
MyFunctional: {
functional: true,
render: (h, context) => h('span', context.data, 'Functional')
}
},
directives: {
myDirective (el) {
el.setAttribute('name', 'Lisa')
}
}
}
const v = new Vue({
el: '#app',
components: {
MyComponent
}
})
document.getElementById('markup1').innerText = v.$el.innerHTML
const cmp1 = VueTestUtils.mount(MyComponent)
document.getElementById('markup2').innerText = cmp1.html()
const cmp2 = VueTestUtils.shallowMount(MyComponent)
document.getElementById('markup3').innerText = cmp2.html()
#markup1, #markup2, #markup3 {
border: 1px solid #777;
margin: 10px;
padding: 10px;
}
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<script src="https://unpkg.com/vue-template-compiler#2.6.10/browser.js"></script>
<script src="https://unpkg.com/#vue/test-utils#1.0.0-beta.29/dist/vue-test-utils.iife.js"></script>
<div id="app">
<my-component></my-component>
</div>
<div id="markup1"></div>
<div id="markup2"></div>
<div id="markup3"></div>
I haven't really looked at the code for vue-test-utils before but stepping through in the debugger makes me suspicious of this line:
https://github.com/vuejs/vue-test-utils/blob/9dc90a3fd4818ff70e270568a2294b1d8aa2c3af/packages/create-instance/create-component-stubs.js#L99
This is the render function for the stubbed child component. It would appear that context.data.directives does contain the correct directive but they aren't being passed on in the call to h.
Contrast that with the render function in my example component MyFunctional, which passes on all of data. That's required for directives to work with a functional component but when MyFunctional gets replaced with a stub the new render function seems to drop the directives property.
The only workaround I've been able to come up with is to provide your own stub:
VueTestUtils.shallowMount(MyComponent, {
stubs: {
BButton: { render: h => h('div')}
}
})
By using a non-functional stub the directive works fine. Not sure how much value this would take away from the test though.

Vue Multiselect: How to send console.log once selection has been made

Using Vue Multiselect, I am trying to send a console.log once I have made a selection. I thought it would work by putting it in the watch but it does not work. Where should it be placed. Please see my component below.
Component
<template>
<div>
<label v-for="topic in topics" class="radio-inline radio-thumbnail" style="background-image: url('http://s3.hubsrv.com/trendsideas.com/profiles/74046767539/photo/3941785781469144249_690x460.jpg')">
<input type="radio" v-model="internalValue" name="topics_radio" :id="topic.id" :value="topic.name">
<span class="white-color lg-text font-regular text-center text-capitalize">{{ topic.name }}</span>
</label>
</div>
</template>
<script>
export default {
props: ['value'],
data () {
return {
internalValue: this.value,
topics: []
}
},
mounted(){
axios.get('/vuetopics').then(response => this.topics = response.data);
},
watch: {
internalValue(v){
this.$emit('input', v);
console.log('topic has been chosen!!!');
}
}
}
</script>
It fires events, so you may catch them.
<multiselect ... #select="doSomething" ...>
Then add your method
...
methods: {
doSomething(selectedOption, id) {
console.log(selectedOption);
}
}
Make sure you implemented vue-multiselect correctly, I don't see the component in your code.