Show on several elements in the same component in vuejs - vue.js

Looping out a number of boxes within the same component in vuejs.
Each box has a button that reveals more text using v-on:click.
The problem is that all the boxes respond to this at the same time.
Is there a way to target the specific button being clicked if there are several buttons in a component?
Is there some way to isolate each button so they all dont activate at once?
<div class="filter-list-area">
<button #click="show =!show"> {{text}} </button>
<ul class="filter-list-item-area">
<li class="filter-list-item " v-for="(items, key) in packages">
<div>
<img class="fit_rating">
</div>
<div class="filter-list-item-info" >
<h3>{{items.partner_university}}</h3>
<p> Match: {{items.match_value}}</p>
<div v-for="(courses, key) in courses">
<transition name="courses">
<div class="courses2" v-show="show">
<p v-if="courses.Pu_Id === items.pu_Id">
{{courses.Course_name}}
</p>
</div>
</transition>
</div>
</div>
</li>
</ul>
</div>
</template>
<script>
import testdata from '../App.vue'
export default {
data (){
return{
text: 'Show Courses',
testFilter: 'Sweden',
show: false
}
},
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
testuni: Array,
list: Array,
packages: Array,
courses: Array
},
methods:{
afunction(){
console.log(this.show);
},
changeText(){
if(this.show){
this.text = 'Hide Courses'
}
else{
this.text = "Show Courses"
}
}
},
mounted() {
this.afunction();
},
watch: {
show:
function() {
this.afunction()
this.changeText()
}
},
}

EDIT: I've created this before you posted the code example, but you could use same principle:
In your data add showMoreText, which will be used to track if show more data should be shown.
I would agree with #anaximander that you should use child components here
Simple example how to show/hide
<template>
<div>
<div v-for="(box, index) in [1,2,3,4,5]">
<div>
Box {{ box }} <button #click="toggleMore(index)">More</button>
</div>
<div v-show="showMoreText[index]">
More about box {{ box }}
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
showMoreText: {},
}
},
methods: {
toggleMore(index) {
/*
Adds a property to a reactive object, ensuring the new property is
also reactive, so triggers view updates.
https://vuejs.org/v2/api/#Vue-set
*/
this.$set(this.showMoreText, index, ! this.showMoreText[index])
}
}
}
</script>

This sounds like an ideal situation for a new child component, which will allow each instance of the new component to have its own separate state.
The child components can emit events to the parent, if cross-component communication is necessary.

Related

Vue: Toggle Button with Data from Child to Parent

I have a parent component that has numerous containers. Each container has an image and some buttons.
I have over simplified the parent and child components below. When a button is clicked that is within the child component, I would like to toggle the class on an element that is in the parent container. I would like to effect each image individually, not globally. How do I do this?
parent:
<template>
<div>
<div :class="{ active: mock }">
<img src="/path">
</div>
<toggleButtons/>
</div>
<div>
<div :class="{ active: mock }">
<img src="/path">
</div>
<toggleButtons/>
</div>
</template>
<script>
import toggleButtons from './toggleButtons'
export default {
name: "parent",
components: {
toggleButtons
}
};
</script>
child:
<template>
<div class="switch-type">
<a #click="mock = false">Proto</a>
<a #click="mock = true">Mock</a>
</div>
</template>
<script>
export default {
name: "toggleButtons",
data() {
return {
mock: false
}
}
};
</script>
Oversimplified example of how you can pass data from child to parent:
Child:
<a #click="$emit('mockUpdated', false)">Proto</a>
<a #click="$emit('mockUpdated', true)">Mock</a>
Parent (template):
<toggleButtons #mockUpdated="doSomething" />
Parent(methods):
doSomething(value) {
// value will be equal to the second argument you provided to $emit in child
}
EDIT: (toggling the class for each individual container):
I would probably create a new component for the container (container.vue), pass a path as a prop :
<template>
<div>
<div :class="{ active: mock }">
<img :src="path">
</div>
<toggleButtons #mockUpdated="doSomething" />
</div>
</template>
<script>
export default {
props: {
path: String,
},
data() {
return {
mock: false
}
},
methods: {
doSomething(value) {
this.mock = value;
}
}
}
</script>
and then in Parent.vue, you can import the container component and use it like:
<template>
<Container path="/path-to-file.jpg" />
<Container path="/path-to-file.jpg" />
</template>
There is nothing to do with slots in your case. You need "emit event" to parent from button, and pass mock data to update img states. Slot is a very different thing. There are multiple ways to achive your goal. One way can be something like this:
parent
<AppImage v-for="img in imageData" :key="img.id" :image="img"/>
data() {
iamges: ["yourPath1", "yourPath2"]
},
computed: {
imageData() {
// adding ID is almost always a good idea while creating Vue apps.
return this.images.map(x => {
id: Math.floor(Math.random() * 1000),
path: x
})
}
}
Image.vue
<img :path="image.path" :class={ active: mock} />
<toggleButtons #toggled=changeImageState />
props: [image],
data() {
mock: ''
},
methods: {
changeImageState(event) {
this.mock = event
}
}
ToggleButtons.vue
<a #click="toggleMock(false)">Proto</a>
<a #click="toggleMock(true)">Mock</a>
emits: ['toggled']
methods: {
toggleMock(val) {
this.$emits('toggled', val)
}
}
Please read the code and let me know if you have any question.

Vue 3 watched property does not update

In the pen i've created, the child component does two things, it watches the props and has a method which emits two values.
The parent handling the emitted values modifies the prop and changes the component. However, the child component never fires its watch event.
See this pen
The logs:
"Emiting values up"
"Form state listener fired"
"Form state has changed"
"move listener fired"
"activeComponent has changed"
"Foo: About to unmount"
"Foo: Unmounted"
I expect between
"Form state listener fired"
"Form state has changed"
a log saying "Foo: formState changed".
How come the watcher is not firing?
I built a couple of test components in my Vue 2 CLI sandbox application, but hopefully it will translate enough to Vue 3 to help you solve your problem. Also, not sure if it is contributing to your problem, but you may want to take a look at this Event Names documentation.
Parent.vue
<template>
<div class="parent">
<h3>Parent.vue</h3>
<hr>
<child :formState="formState" #form-state-event="updateFormState" />
</div>
</template>
<script>
import Child from './Child.vue'
export default {
components: {
Child
},
data() {
return {
formState: 'Initial State'
}
},
methods: {
updateFormState(newState) {
console.log('updateFormState');
this.formState = newState;
}
}
}
</script>
Child.vue
<template>
<div class="child">
<h3>Child.vue</h3>
<div class="row">
<div class="col-md-6">
<label>Form State From Parent (Prop):</label>
<span>{{ dataFormState }}</span>
</div>
</div>
<div class="row">
<div class="col-md-6">
<button type="button" class="btn btn-sm btn-secondary" #click="changeAndEmit">Emit new form state</button>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
formState: {
type: String,
required: true
}
},
data() {
return {
dataFormState: this.formState,
clickCounter: 1
}
},
watch: {
formState(newValue) {
this.dataFormState = newValue;
}
},
methods: {
changeAndEmit() {
console.log('Emitting values up')
this.$emit("form-state-event", "New form state " + this.clickCounter++);
}
},
}
</script>
<style scoped>
label {
font-weight: bold;
margin-right: 0.5rem;
}
</style>

Removing specific object from array keeps removing last item

Here is what I have and I will explain it as much as I can:
I have a modal inside my HTML code as shown below:
<div id="favorites-modal-edit" class="modal">
<div class="modal-background"></div>
<div class="modal-card px-4">
<header class="modal-card-head">
<p class="modal-card-title">Favorites</p>
<button class="delete" aria-label="close"></button>
</header>
<section class="modal-card-body">
<div class="container">
<div id="favorites-modal-edit-wrapper" class="columns is-multiline buttons">
<favorites-edit-component v-for="(favorite, index) in favorites_list" :key="favorite.id" :favorite="favorite" />
</div>
</div>
</section>
<footer class="modal-card-foot">
<button class="button" #click="addItem">
Add Item
</button>
</footer>
</div>
</div>
The id="favorites-modal-edit" is the Vue.js app, then I have the <favorites-edit-component /> vue.js component.
Here is the JS code that I have:
I have my favorites_list generated which is an array of objects as shown below:
const favorites_list = [
{
id: 1,
name: 'Horse',
url: 'www.example.com',
},
{
id: 2,
name: 'Sheep',
url: 'www.example2.com',
},
{
id: 3,
name: 'Octopus',
url: 'www.example2.com',
},
{
id: 4,
name: 'Deer',
url: 'www.example2.com',
},
{
id: 5,
name: 'Hamster',
url: 'www.example2.com',
},
];
Then, I have my vue.js component, which is the favorites-edit-component that takes in the #click="removeItem(this.index) which is coming back as undefined on the index.
Vue.component('favorites-edit-component', {
template: `
<div class="column is-half">
<button class="button is-fullwidth is-danger is-outlined mb-0">
<span>{{ favorite.name }}</span>
<span class="icon is-small favorite-delete" #click="removeItem(this.index)">
<i class="fas fa-times"></i>
</span>
</button>
</div>
`,
props: {
favorite: Object
},
methods: {
removeItem: function(index) {
this.$parent.removeItem(index);
},
}
});
Then I have the vue.js app that is the parent as shown below:
new Vue({
el: '#favorites-modal-edit',
// Return the data in a function instead of a single object
data: function() {
return {
favorites_list
};
},
methods: {
addItem: function() {
console.log('Added item');
},
removeItem: function(index) {
console.log(index);
console.log(this.favorites_list);
this.favorites_list.splice(this.favorites_list.indexOf(index), 1);
},
},
});
The problem:
For some reason, each time I go to delete a item from the list, it's deleting the last item in the list and I don't know why it's doing it, check out what is happening:
This is the guide that I am following:
How to remove an item from an array in Vue.js
The item keeps coming back as undefined each time the remoteItem() function is triggered as shown below:
All help is appreciated!
There is an error in your favorites-edit-component template, actually in vue template, when you want to use prop, data, computed, mehods,..., dont't use this
=> there is an error here: #click="removeItem(this.index)"
=> in addition, where is index declared ? data ? prop ?
you're calling this.$parent.removeItem(index); then in removeItem you're doing this.favorites_list.splice(this.favorites_list.indexOf(index), 1); this means that you want to remove the value equal to index in you array no the value positioned at the index
=> this.favorites_list[index] != this.favorites_list[this.favorites_list.indexOf(index)]
In addition, I would suggest you to modify the favorites-edit-component component to use event so it can be more reusable:
favorites-edit-component:
<template>
<div class="column is-half">
<button class="button is-fullwidth is-danger is-outlined mb-0">
<span>{{ favorite.name }}</span>
<span class="icon is-small favorite-delete" #click="$emit('removeItem', favorite.id)">
<i class="fas fa-times"></i>
</span>
</button>
</div>
</template>
and in the parent component:
<template>
...
<div id="favorites-modal-edit-wrapper" class="columns is-multiline buttons">
<favorites-edit-component
v-for="favorite in favorites_list"
:key="favorite.id"
:favorite="favorite"
#removeItem="removeItem($event)"
/>
</div>
...
</template>
<script>
export default {
data: function () {
return {
favorites_list: [],
};
},
methods: {
...
removeItem(id) {
this.favorites_list = this.favorites_list.filter((favorite) => favorite.id !== id);
}
...
},
};
I would restructure your code a bit.
In your favorites-edit-component
change your removeItem method to be
removeItem() {
this.$emit('delete');
},
Then, where you are using your component (in the template of the parent)
Add an event catcher to catch the emitted "delete" event from the child.
<favorites-edit-component v-for="(favorite, index) in favorites_list" :key="favorite.id" :favorite="favorite" #delete="removeItem(index)"/>
The problem you have right now, is that you are trying to refer to "this.index" inside your child component, but the child component does not know what index it is being rendered as, unless you specifically pass it down to the child as a prop.
Also, if you pass the index down as a prop, you must refer to it as "index" and not "this.index" while in the template.

Vuejs How to use $emit function properly?

I have two components SingleTaskItem and ControlArea. ControlArea has a collapse button and when that button is clicked I want to call a function in SingleTaskItem. Here is my code so far. Can you please tell me what am I doing wrong?
SingleTaskItem:
<template>
<div class="SingleTaskItem">
<ControlArea v-bind:collapsed="collapsed"
v-bind:onClickCollapse="onClickCollapse"/>
</div>
</template>
<script>
export default {
name: "SingleTaskItem",
data() {
return {
collapsed: false
};
},
methods: {
onClickCollapse(value) {
console.log("on Click Collapse called");
this.collapsed = value;
}
}
};
</script>
ControlArea:
<template>
<div class="ControlArea">
<div class="action-btn edit">
<i class="fas fa-ellipsis-h"></i>
</div>
<div class="action-btn collapsible">
<i v-if="collapsed" v-on:click="uncollapse" class="fas fa-chevron-down"></i>
<i v-else v-on:click="collapse" class="fas fa-chevron-up"></i>
</div>
</div>
</template>
<script>
export default {
name: "ControlArea",
props: {
collapsed: Boolean
},
methods: {
collapse(event) {
console.log("collapse function is called");
this.$emit("onClickCollapse", "true");
},
uncollapse(event) {
this.$emit("onClickCollapse", "false");
}
}
};
</script>
Instead of v-bind:onClickCollapse="onClickCollapse" you should use v-on:onClickCollapse. This is kind of easy to miss because you used the word 'on' in your event name - it might be clearer to remove that.
Also, to pass that true/false string you need to pass $event into your function call: v-on:onClickCollapse($event). To clean this up you should probably also pass true/false booleans rather than strings.

Send data from one component to another in vue

Hi I'm trying to send data from one component to another but not sure how to approach it.
I've got one component that loops through an array of items and displays them. Then I have another component that contains a form/input and this should submit the data to the array in the other component.
I'm not sure on what I should be doing to send the date to the other component any help would be great.
Component to loop through items
<template>
<div class="container-flex">
<div class="entries">
<div class="entries__header">
<div class="entries__header__title">
<p>Name</p>
</div>
</div>
<div class="entries__content">
<ul class="entries__content__list">
<li v-for="entry in entries">
{{ entry.name }}
</li>
</ul>
</div>
<add-entry />
</div>
</div>
</template>
<script>
import addEntry from '#/components/add-entry.vue'
export default {
name: 'entry-list',
components: {
addEntry
},
data: function() {
return {
entries: [
{
name: 'Paul'
},
{
name: 'Barry'
},
{
name: 'Craig'
},
{
name: 'Zoe'
}
]
}
}
}
</script>
Component for adding / sending data
<template>
<div
class="entry-add"
v-bind:class="{ 'entry-add--open': addEntryIsOpen }">
<input
type="text"
name="addEntry"
#keyup.enter="addEntries"
v-model="newEntries">
</input>
<button #click="addEntries">Add Entries</button>
<div
class="entry-add__btn"
v-on:click="openAddEntry">
<span>+</span>
</div>
</div>
</template>
<script>
export default {
name: 'add-entry',
data: function() {
return {
addEntryIsOpen: false,
newEntries: ''
}
},
methods: {
addEntries: function() {
this.entries.push(this.newEntries);
this.newEntries = '';
},
openAddEntry() {
this.addEntryIsOpen = !this.addEntryIsOpen;
}
}
}
</script>
Sync the property between the 2:
<add-entry :entries.sync="entries"/>
Add it as a prop to the add-entry component:
props: ['entries']
Then do a shallow merge of the 2 and emit it back to the parent:
this.$emit('entries:update', [].concat(this.entries, this.newEntries))
(This was a comment but became to big :D)
Is there a way to pass in the key of name? The entry gets added but doesn't display because im looping and outputting {{ entry.name }}
That's happening probably because when you pass "complex objects" through parameters, the embed objects/collections are being seen as observable objects, even if you sync the properties, when the component is mounted, only loads first level data, in your case, the objects inside the array, this is performance friendly but sometimes a bit annoying, you have two options, the first one is to declare a computed property which returns the property passed from the parent controller, or secondly (dirty and ugly but works) is to JSON.stringify the collection passed and then JSON.parse to convert it back to an object without the observable properties.
Hope this helps you in any way.
Cheers.
So with help from #Ohgodwhy I managed to get it working. I'm not sure if it's the right way but it does seem to work without errors. Please add a better solution if there is one and I'll mark that as the answer.
I follow what Ohmygod said but the this.$emit('entries:update', [].concat(this.entries, this.newEntries)) didn't work. Well I never even need to add it.
This is my add-entry.vue component
<template>
<div
class="add-entry"
v-bind:class="{ 'add-entry--open': addEntryIsOpen }">
<input
class="add-entry__input"
type="text"
name="addEntry"
placeholder="Add Entry"
#keyup.enter="addEntries"
v-model="newEntries"
/>
<button
class="add-entry__btn"
#click="addEntries">Add</button>
</div>
</template>
<script>
export default {
name: 'add-entry',
props: ['entries'],
data: function() {
return {
addEntryIsOpen: false,
newEntries: ''
}
},
methods: {
addEntries: function() {
this.entries.push({name:this.newEntries});
this.newEntries = '';
}
}
}
</script>
And my list-entries.vue component
<template>
<div class="container-flex">
<div class="wrapper">
<div class="entries">
<div class="entries__header">
<div class="entries__header__title">
<p>Competition Entries</p>
</div>
<div class="entries__header__search">
<input
type="text"
name="Search"
class="input input--search"
placeholder="Search..."
v-model="search">
</div>
</div>
<div class="entries__content">
<ul class="entries__content__list">
<li v-for="entry in filteredEntries">
{{ entry.name }}
</li>
</ul>
</div>
<add-entry :entries.sync="entries"/>
</div>
</div>
</div>
</template>
<script>
import addEntry from '#/components/add-entry.vue'
import pickWinner from '#/components/pick-winner.vue'
export default {
name: 'entry-list',
components: {
addEntry,
pickWinner
},
data: function() {
return {
search: '',
entries: [
{
name: 'Geoff'
},
{
name: 'Stu'
},
{
name: 'Craig'
},
{
name: 'Mark'
},
{
name: 'Zoe'
}
]
}
},
computed: {
filteredEntries() {
if(this.search === '') return this.entries
return this.entries.filter(entry => {
return entry.name.toLowerCase().includes(this.search.toLowerCase())
})
}
}
}
</script>