Toggle in loop? - vue.js

I wish to toggle (show/hide) a list when clicking on a title, but cant get the following to work
I have this:
<!-- Title -->
<div v-for="(subitem, index) in item" v-if="index === 0" #click="toggle(subitem)">
{{subitem.system_name}} - ({{item.length}})
</div>
<!-- All title items that should expand on click "Title" -->
<div v-if="subitem.clicked">
<p>{{subitem.system_name}}</p>
</div>
When clicking on the im triggering a toggle function called toggle, that sets a property on the item "clicked" to true or false (I should mention that this property does not already exist on the object, and I haven't got the possiblity add it, as we get the JSON from an API)
The toggle function is this:
toggle: function (data) {
data.clicked = !data.clicked;
},
Now, when I do this above, I get an error saying: "Property or method "subitem" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option"
Im guessing I get this because the "clicked" property doesnt exist in the object... So how do I work around this? Can't see any real solution ?

You initialize subitem in the v-for as a single item in the loop, but you are using it outside the element which has v-for loop on it. That's the reason you get that warning:
Property or method "subitem" is not defined on the instance but referenced during render.
Make sure to declare reactive data properties in the data option"
So move the div you want to toggle inside the div which has the v-for loop on it
<!-- Title -->
<div v-for="(subitem, index) in item" v-if="index === 0" #click="toggle(subitem)">
{{subitem.system_name}} - ({{item.length}})
<!-- All title items that should expand on click "Title" -->
<div v-if="subitem.clicked">
<p>{{subitem.system_name}}</p>
</div>
</div>
And coming yo the 2nd issue, as you mention the subitem obj does not have clicked property when you fetch the json from api.
You cannot add root level reactive properties after the vue instance is created.
Since you want to toggle the appeance of the div based on the property clicked which is not available at the time vue instance is created you should use vm.$set() to add reactive properties or Object.assign() to add properties to existing object. See Reactivity in depth
So in your case
toggle: function (data) {
if(data.hasOwnProperty('clicked')){
data.clicked = !data.clicked;
}else{
//since its the first time , set the value pf clicked to true to show the subitem
data = Object.assign({}, data, {clicked: true});
}
},

Related

Getting element (button) data attribute with vue3

I have the following html div. The {{ }} represent liquid syntax and it renders server side.
<div class="two columns">
<button
data-value='{{ value }}'
class="button-selection"
:class="selectionButtonClass($event)"
#click="selectionButton($event)"
>
<span class="text">{{ value }}</span>
</button>
</div>
In a vue 3 instance I have the following method in the same page.
selectionButtonClass(el) {
console.log('checking val');
console.log(el);
}
My goal is to set a conditional class in the selectionButton method but I can't get the element to get the data attribute. The above appears in the console log as undefined. However the #click does show the event obviously it's recognize the onclick but not the class method check.
$event is only available to event handlers. #click="selectionButton($event)" defines an inline event handler, while :class="selectionButtonClass($event)" is not an event handler.
To get the element, you need to add a ref attribute to the <button>:
<button
ref="selectionButton"
data-value='{{ value }}'
class="button-selection"
:class="selectionButtonClass($event)"
#click="selectionButton($event)"
>
And access it by this.$refs.selectionButton, assuming you are using the options API. However, the ref is available only after the component is mounted. Thus you need to handle the case where the ref is null.
More on template refs: https://vuejs.org/guide/essentials/template-refs.html
Since you are using server side rendering, I think it would be better to render the value as a parameter of the selectionButton function on the server side.

Expand all Bootstrap-Vue b-table rows

I have a b-table which looks something like this:
<b-table :fields="fields" :items="tableRows">
<template #head(expand_all+)="data">
//would like to make this a button to expand all row-details
<div class="text-right">{{data.label}}</div>
</template>
...
<template #cell(expand_all+)="data">
<div class="expand-arrow-container":class="{ 'expanded': data.detailsShowing }" #click="data.toggleDetails">
<font-awesome-icon icon="chevron-right" />
</div>
</template>
...
<template #row-details="row">
//display data here
</template>
</b-table>
Now, all of this works fine and and my row details expand/contract as I would expect (see b-table row-details for more information).
The problem arises in that I want to be able to click one button and expand/contract all the row-details. My plan is to change template #head(expand_all+)="data" into a button which can be clicked to accomplish this. However, I do not know how to do this. I have searched and read the b-table documentation and have not found any way of accomplishing what I want. I've even taken a look at the table object to see if it had any references to its rows and I didn't see anything.
Do any of you know a way to accomplish this? Getting a reference to the rows would make this a simple problem, but as I mentioned I didn't see anything. I am also not looking to swap out the b-table with a bunch of b-collapse elements since I have put a lot of work into the table.
As described on the docs, you can set a _showDetails property on your items passed to the table. Which if set to true will expand that row's details. This means you could loop through all your items and set that property to true to expand all rows. And do the same with false to collapse them.
If the record has its _showDetails property set to true, and a row-details scoped slot exists, a new row will be shown just below the item, with the rendered contents of the row-details scoped slot.
Here's two example methods you could use.
We're using $set because we're adding a new property to an already existing object. You can read more about why here
expandAll() {
for(const item of this.tableRows) {
this.$set(item, '_showDetails', true)
}
},
collapseAll() {
for(const item of this.tableRows) {
this.$set(item, '_showDetails', false)
}
}

VueJS - toggle background image of list item on hover

I'm relatively new to Vue and I'm wondering what's wrong with my component that my isHover variable (prop?) isn't working to change the background on mouseover.
<template>
<div class="list-wrap" v-if="gridItems">
<div
class="list-itme"
v-for="(item, index) in gridItems"
:key="index"
#click.stop="setCurrentLocation(location)"
>
<a
#mouseover="mouseOver(index)"
#mouseleave="mouseLeave(index)"
:style="{
background: isHover
? `url(${item.location_image.thumbnails.large.url})`
: `url(${item.location_image.thumbnails.largeHover.url})`
}"
>
{{ item.location_name }}
{{ isHover }}
</a>
</div>
</div>
</template>
<script>
export default {
name: "GridItems",
computed: mapState(["filters", "GridItems"]),
methods: {
mouseOver(index) {
this.item[index].isHover = true;
},
mouseLeave(index) {
this.item[index].isHover = false;
}
},
data() {
return {
isHover: false
};
}
};
</script>
background: isHover
? `url(${item.location_image.thumbnails.large.url})`
: `url(${item.location_image.thumbnails.largeHover.url})`
The isHover above references the data property of the component.
Your mouseOver() and mouseLeave() methods are assigning a property also called isHover on this.item[index]. These are two completely different properties. Where are you getting this.item from? I don't see any props or it being declared as a data attribute.
Edit
You could have a isHover property on the gridItem. Therefore instead of passing index as an argument into the mouse event methods you can actually pass item. Then just set item.isHover = true. On the style binding you can just check against item.isHover.
Which means you don't need the "other" isHover data property on the component.
There are a few things to consider in your code, the isHover variable which is being used to change the background of your elements is a data property, but in your mouseOver and mouseLeave you are trying to change the isHover property from an element on an array called item which is not declared in the code you posted. Another thing to notice is that it is not necessary to return anything on your mouseOver and mouseLeave methods.
As I understand, the expected behavior of your code is to change the background color of the item you are hovering with your cursor. A couple of suggestions, you should use class binding instead of adding inline styles to your template elements, also you could pass the item instead of the index on your mouseover and mouseleave handlers. Another thing to mention is that I would only recommend doing this if for some reason you need the isHover property on your item for something else, otherwise you should just use CSS :hover to achieve this. I made a small demo so you can take a look on what you can do to make your code work: codepen
Edit
To change the image when hovering over an item you should be using the isHover property of that particular item instead of the component's data property isHover which you are currently using to try to change the image url. I updated my codepen.

How to get value from a v-for loop in Vue JS

I need to access data from a v-for loop in a Vue template. I can't figure out how though..
This is what I have so far:
Vue.component('artikel-lista', {
template:`
<ul>
<artikel v-for="artikel in artiklar">{{ artikel.title }} Pris: {{artikel.price}} <a :href="artikel.link" target="_blank" class="button tiny">Läs mer</a></artikel>
</ul>
`,
data(){
return{
artiklar: App.artiklar
}
},
created(){
Event.$on('isSelected', function() {
// here I need to access for example artikel.title from the v-for loop
});
},
});
The event listener Event.$on() is receiving an event (that part works). When this happens I want to get the value from for example artikel.title in the v-for loop. How can I do that?
EDITED:
Some more info about the problem... I have a list of items, each preceded by a checkbox. I need to get the values from the checked li elements. So I emit an event when the checkbox is clicked in one Vue component, and listen for the event in another Vue component, where I would like to fetch the current 'artikel' value and store it.
I realize now that need to rethink this and instead see which checkboxes are checked when clicking a 'submit' button (Add items) and not when the checkbox is clicked.
But the problem of getting values from the v-for loop outside of the loop remains I think...
This is the other Vue Component (that sends the event):
Vue.component('artikel',{
template: '<li style="list-style-type:none;"><input #change="isSelected" type="checkbox" class="kryssruta" /><slot></slot></li>',
methods:{
isSelected(){
Event.$emit('isSelected');
},
}
});
The first line of code is this:
window.Event = new Vue();
So that I can send events between components using "Event".
The custom element <artikel-lista></artikel-lista> is used in a third Vue component. The result is a list of items with a checkbox in front of each list item.

Is it posible to delete `div` from template?

I have a component myHello:
<temlate>
<div>
<h2>Hello</h1>
<p>world</p>
</div>
</template>
And main component:
<h1>my hello:</h1>
<my-hello><my-hello>
After rendering shows this:
<h1>my hello:</h1>
<div>
<h2>Hello</h1>
<p>world</p>
</div>
How to delete <div> ?
With VueJS, every component must have only one root element. The upgrade guide talks about this. If it makes you feel better, you are not alone. For what it's worth the components section is a good read.
With the myriad of solutions to your problem, here is one.
component myHello:
<temlate>
<h2>Hello</h1>
</template>
component myWorld:
<temlate>
<p>world</p>
</template>
component main
<h1>my hello:</h1>
<my-hello><my-hello>
<my-world><my-world>
Vue gives you the tools to do so by creating templates or you can do it by having a parent div with two parent divs as children. Reset the data from the data function. Stick with convention (create templates). It's hard to get used to use Vue when you have a jQuery background. Vue is better
Ex.
data () {
message: 'My message'
}
When you click a button to display a new message. Clear the message or just set the message variable with a new value.
ex. this.message = 'New Message'
If you like to show another div. you should used the if - else statement and add a reactive variable. Ex. showDivOne
data () {
message: 'My message'
showDivOne: true,
showDivTwo: false
}
Add this reactive variables to the parent divs that corresponds to the div.
When clicking the button, you should have a function like...
methods: {
buttonClick () {
this.showDivOne = false
this.showDivTwo = true
}
}
I think you can use v-if directive to controll. Add a flag to controll status to show or hide