Vuejs - Accordion - javascript

I'm trying to create an accordion using vuejs.
I found some examples online, but what I want is different. For SEO purpose I use "is" and "inline-template", so the accordion is kind of static not fully created in Vuejs.
I have 2 problems/questions:
1) I need to add a class "is-active" on the component based on user interaction(clicks), because of this I receive the following error.
Property or method "contentVisible" is not defined on the instance but
referenced during render. Make sure to declare reactive data
properties in the data option.
This probable because I need to set it at instance level. But "contentVisible" have a value (true or false) different for each component.
So I thought using at instance level an array of "contentVisible" and a props (pass thru instance) and custom events on child to update the instance values.
2) Could work but it is a static array. How can I make a dynamic array (not knowing the number of item components) ?
<div class="accordion">
<div>
<div class="accordion-item" is="item" inline-template :class="{ 'is-active': contentVisible}" >
<div>
<a #click="toggle" class="accordion-title"> Title A1</a>
<div v-show="contentVisible" class="accordion-content">albatros</div>
</div>
</div>
<div class="accordion-item" is="item" inline-template :class="{ 'is-active': contentVisible}" >
<div>
<a #click="toggle" class="accordion-title"> Title A2</a>
<div v-show="contentVisible" class="accordion-content">lorem ipsum</div>
</div>
</div>
</div>
var item = {
data: function() {
return {
contentVisible: true
}
},
methods: {
toggle: function(){
this.contentVisible = !this.contentVisible
}
}
}
new Vue({
el:'.accordion',
components: {
'item': item
}
})
Update
I create the following code but the custom event to send the modification from component to instance is not working, tabsactive is not changing
var item = {
props: ['active'],
data: function() {
return {
contentVisible: false
}
},
methods: {
toggle: function(index){
this.contentVisible = !this.contentVisible;
this.active[index] = this.contentVisible;
**this.$emit('tabisactive', this.active);**
console.log(this.active);
}
}
}
new Vue({
el:'.accordion',
data: {
tabsactive: [false, false]
},
components: {
'item': item
}
})
<div class="accordion" **#tabisactive="tabsactive = $event"**>
<div class="accordion-item" is="item" inline-template :active="tabsactive" :class="{'is-active': tabsactive[0]}">
<div>
<a #click="toggle(0)" class="accordion-title"> Title A1</a>
<div v-show="contentVisible" class="accordion-content">albatros</div>
</div>
</div>
<div class="accordion-item" is="item" inline-template :active="tabsactive" :class="{'is-active': tabsactive[1]}">
<div>
<a #click="toggle(1)" class="accordion-title" > Title A2</a>
<div v-show="contentVisible" class="accordion-content">lorem ipsum</div>
</div>
</div>
</div>

This works for me:
<template>
<div>
<ul>
<li v-for="index in list" :key="index._id">
<button #click="contentVisible === index._id ? contentVisible = false : contentVisible = index._id">{{ index.title }}</button>
<p v-if='contentVisible === index._id'>{{ index.item }}</p>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "sameName",
data() {
return {
contentVisible: false,
list: [
{
_id: id1,
title: title1,
item: item1
},
{
_id: id2,
title: title2,
item: item2
}
]
};
},
};
</script>

On point 1:
You have to define contentVisible as a vue instance variable, as you have accessed it with vue directive v-show, it searches this in vue data, watchers, methods, etc, and if it does not find any reference, it throws this error.
As your accordion element is associated with the parent component, you may have to add contentVisible data there, like following:
new Vue({
el:'.accordion',
data: {
contentVisible: true
}
components: {
'item': item
}
})
If you have multiple items, you may use some other technique to show one of them, like have a data variable visibleItemIndex which can change from 1 to n-1, where n is number of items.
In that case, you will have v-show="visibleItemIndex == currentIndex" in the HTML.
You can as well have hash for saving which index are to de displayed and which to be collapsed.
On point 2:
You can use v-for if you have dynamic arrays. you can see the documentation here.

I'm having a real hard time understanding what exactly it is you want or why you would want it, but I think this does it?
Vue.component('accordion-item', {
template: '#accordion-item',
methods: {
toggle() {
if(this.contentVisible){
return
}
if(this.$parent.activeTab.length >= 2){
this.$parent.activeTab.shift()
}
this.$parent.activeTab.push(this)
}
},
computed: {
contentVisible() {
return this.$parent.activeTab.some(c => c === this)
}
}
})
const Accordion = Vue.extend({
data() {
return {
activeTab: []
}
},
methods: {
handleToggle($event) {
this.activeTab = []
}
}
})
document.querySelectorAll('.accordion').forEach(el => new Accordion().$mount(el))
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
<template id="accordion-item">
<div class="accordion-item" :class="{ 'is-active': contentVisible}">
<slot name="title"></slot>
<div v-show="contentVisible" class="accordion-content" #click="$emit('toggle', $event)">
<slot name="content"></slot>
</div>
</div>
</template>
<div class="accordion">
<accordion-item #toggle="handleToggle">
<p slot="title">a title</p>
<p slot="content">there are words here</p>
</accordion-item>
<accordion-item #toggle="handleToggle">
<p slot="title">titles are for clicking</p>
<p slot="content">you can also click on the words</p>
</accordion-item>
<accordion-item #toggle="handleToggle">
<p slot="title">and another</p>
<p slot="content">only two open at a time!</p>
</accordion-item>
<accordion-item #toggle="handleToggle">
<p slot="title">and #4</p>
<p slot="content">amazing</p>
</accordion-item>
</div>

Related

Vue - Emitting a data object but changing one changes them all

I have a TODO app and want to pass by props from one component to another an array of objects. An object is added every time you click a button but I'm having trouble with it. The problem is that the property value becomes the same for every single object added to the array. It seems like it's not saving correctly each tareas.tarea data.
App.vue
<template>
<div>
<Header></Header>
<AgregarTarea #tareaAgregada="agregarTarea"></AgregarTarea>
<div class="container">
<div class="columns">
<div class="column">
<Lista :tareas = 'tareas' #eliminarItem="eliminarTarea"></Lista>
<!-- here i pass through props the array of objects -->
</div>
<div class="column">
<TareaFinalizada></TareaFinalizada>
{}
</div>
</div>
</div>
</div>
</template>
<script>
import Header from './components/Header'
import AgregarTarea from './components/AgregarTarea'
import Lista from './components/Lista'
import TareaFinalizada from './components/TareaFinalizada'
export default {
data(){
return {
tareas:[]
}
},
components: {
Header,
AgregarTarea,
Lista,
TareaFinalizada
},
methods: {
agregarTarea(data){
//add new object to the array
this.tareas.push(data)
},
eliminarTarea(data) {
this.tareas.splice(data.id, 1);
}
}
};
</script>
AgregarTarea.vue || Here is where i add a new ToDo
<template>
<div class="container">
<input class="input" type="text" placeholder="Text input" v-model="tareas.tarea">
<button class="button is-primary" #click="agregarTarea">Agregar Tarea</button>
</div>
</template>
<script>
export default {
data(){
return {
tareas: {
tarea:'',
id:null,
editar:false
}
}
},
methods: {
agregarTarea(){
this.$emit('tareaAgregada', this.tareas)
this.tareas.tarea = ' ';
}
}
}
</script>
Lista.vue || And here is where i display the ToDo's
<template>
<div>
<div class="list is-hoverable">
<ul>
<li v-for="(tarea, index) in tareas" :key="index">
<a class="list-item has-text-centered" #click="editarTexto(index)">
{{ tarea }}
<div class="editar" v-if="editar">
<input class="input" type="text" placeholder="Text input" v-model="nuevaTarea">
</div>
</a>
<button class="button is-danger" #click="eliminarItem(index)">Eliminar</button>
<div><input type="checkbox"> Finalizada</div>
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
props:['tareas'],
data(){
return {
nuevaTarea: ' ',
editar:false,
}
},
methods: {
eliminarItem(index){
this.$emit('eliminarItem', index)
},
editarTexto(){
this.editar = true
}
}
}
</script>
<style scoped>
</style>
JavaScript objects are passed by reference (not cloned by value). Each time you $emit the tareas object from AgregarTarea.vue, it's the same object reference as before, even if the properties have changed. So all of the objects in your tareas array in App.vue are the same object.
To fix this, change AgregarTarea.vue to $emit a clone each time:
methods: {
agregarTarea(){
this.$emit('tareaAgregada', Object.assign({}, this.tareas)) // clone
this.tareas.tarea = ' ';
}
}
(This is a shallow clone and would not work properly if this.tareas had nested objects, but it doesn't.)
Option #2
Here's a different way that works easily for nested objects:
new Vue({
el: "#app",
data(){
return {
tareas: null // <-- It's not filled here
}
},
methods: {
resetTareas() { // <-- it's filled here instead
this.tareas = {
tarea:'',
id:null,
editar:false
}
},
agregarTarea(){
this.$emit('tareaAgregada', this.tareas);
this.resetTareas(); // <-- Create a brand new object after emitting
}
},
created() {
this.resetTareas(); // <-- This is for the first one
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
Tarea: <input type="text" v-model="tareas.tarea" /><br /><br />
<button #click="agregarTarea">Emit</button><br /><br />
Object: {{ tareas }}
</div>
Since resetTareas creates a brand new object every time, you don't have to worry about cloning anything, and it works even if tareas is a complex nested object.

Bind element inside a for loop Vue not working properly

In the following Vue Component I want to loop through dwarfs array. And as long as I am in the current component, everything is fine (TEST) and also all the following properties are correct.
Currenct_Component.vue :
<template>
<div>
<h2>Stamm: {{ tribeName }}</h2>
<div class="card-container">
<div class="card" style="width: 18rem;" v-for="dwarf in dwarfs" :key="dwarf.name">
<!-- TEST -->
<p>{{dwarf}}</p>
<!-- CHILD COMPONENT -->
<app-modal
:showModal="showModal"
:targetDwarf="dwarf"
#close="showModal = false"
#weaponAdded="notifyApp"
/>
<!-- <img class="card-img-top" src="" alt="Card image cap">-->
<div class="card-body">
<h3 class="card-title" ref="dwarfName">{{ dwarf.name }}</h3>
<hr>
<ul class="dwarf-details">
<li><strong>Alter:</strong> {{ dwarf.age }}</li>
<li><strong>Waffen:</strong>
<ul v-for="weapon in dwarf.weapons">
<li><span>Name: {{ weapon.name }} | Magischer Wert: {{ weapon.magicValue }}</span></li>
</ul>
</li>
<li><strong>Powerfactor:</strong> {{ dwarf.weapons.map(weapon => weapon.magicValue).reduce((accumulator, currentValue) => accumulator + currentValue) }}</li>
</ul>
<button class="card-button" #click="showModal = true"><span class="plus-sign">+</span> Waffe</button>
</div>
</div>
</div>
<button id="backBtn" #click="onClick">Zurück</button>
</div>
</template>
<script>
import Modal from './NewWeaponModal.vue';
export default {
data() {
return {
showModal: false,
}
},
components: { appModal : Modal },
props: ['tribeName', 'dwarfs'],
methods: {
onClick() {
this.$emit('backBtn')
},
notifyApp() {
this.showModal = false;
this.$emit('weaponAdded');
}
},
}
</script>
But when I bind the element dwarf to the Child Component <app-modal/> it changes to the next dwarf in the array dwarfs (TEST) - (So as the result when i add a new weapon in the modal-form it gets added to the second dwarf...):
Child_Component.vue :
<template>
<div>
<div class="myModal" v-show="showModal">
<div class="modal-content">
<span #click="$emit('close')" class="close">×</span>
<h3>Neue Waffe</h3>
<!-- TEST -->
<p>{{ targetDwarf }}</p>
<form>
<input
type="text"
placeholder="Name..."
v-model="weaponName"
required
/>
<input
type="number"
placeholder="Magischer Wert..."
v-model="magicValue"
required
/>
<button #click.prevent="onClick">bestätigen</button>
</form>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
weaponName: '',
magicValue: '',
}
},
props: ['showModal', 'targetDwarf'],
methods: {
onClick() {
if(this.weaponName !== '' &&
Number.isInteger(+this.magicValue)) {
let newData = {...this.dwarf};
newData['weapons'] = [
...this.dwarf['weapons'],
{
"name": this.weaponName,
"magicValue": Number.parseInt(this.magicValue)
},
];
this.$http.post("https://localhost:5019/api", newData)
.then(data => data.text())
.then(text => console.log(text))
.catch(err => console.log(err));
this.$emit('weaponAdded');
} else {
alert('You should fill all fields validly')
}
},
}
}
</script>
It looks like you have the <app-modal/> component inside of the v-for="dwarf in dwarfs" loop, but then the control for showing all of the modal components created by that loop is just in one variable: showModal. So when showModal is true, the modal will show each of the dwarfs, and I'm guessing the second dwarf's modal is just covering up the first one's.
To fix this, you could move the <app-modal /> outside of that v-for loop, so there's only one instance on the page, then as part of the logic that shows the modal, populate the props of the modal with the correct dwarf's info.
Something like this:
<div class="card-container">
<div class="card" v-for="dwarf in dwarfs" :key="dwarf.name">
<p>{{dwarf}}</p>
<div class="card-body">
<button
class="card-button"
#click="() => setModalDwarf(dwarf)"
>
Waffe
</button>
</div>
</div>
<!-- Move outside of v-for loop -->
<app-modal
:showModal="!!modalDwarfId"
:targetDwarf="modalDwarfId"
#close="modalDwarfId = null"
#weaponAdded="onDwarfWeaponAdd"
/>
</div>
export default {
//....
data: () => ({
modalDwarfId: null,
)},
methods: {
setModalDwarf(dwarf) {
this.modalDwarfId = drawf.id;
},
onDwarfWeaponAdd() {
//...
}
},
}
You could then grab the correct dwarf data within the modal, from the ID passed as a prop, or pass in more granular data to the modal so it's more "dumb", which is the better practice so that the component isn't dependent on a specific data structure. Hope that helps
Courtesy of #Joe Dalton's answer, a bit alternated for my case:
<div class="card" style="width: 18rem;" v-for="dwarf in dwarfs" :key="dwarf.name">
...
<button class="card-button" #click="setModalDwarf(dwarf)"><span class="plus-sign">+</span> Waffe</button>
<div>
<app-modal
:showModal="showModal"
:targetDwarf="currentDwarf"
#close="showModal = false"
#weaponAdded="notifyApp"
/>
<script>
import Modal from './NewWeaponModal.vue';
export default {
data() {
return {
showModal: false,
currentDwarf: null,
}
},
components: { appModal : Modal },
props: ['tribeName', 'dwarfs'],
methods: {
setModalDwarf(dwarf) {
this.currentDwarf = dwarf;
this.showModal = true;
},
...
}
</script>

VueJS How to show/hide closest hidden element in list of items

How can I show/hide the closest div by clicking a button in vue?
lets say I have a list of items, each with some hidden details
<ul>
<li v-for="item in items" :key="item.id">
<div>
<p>{{item.text}}</p>
<button #click="showDetails(item)">Show details</div>
<div class="details" :class="isVisible ? activeClass : 'hidden'">Some hidden details</div>
</div>
</li>
</ul>
Then I do
data() {
return {
items: [ // a bunch of item objects here]
isVisible: false,
activeClass: 'is-visible'
}
},
methods: {
showDetails(item) {
this.isVisible = item;
}
}
Right now, when I click on on of the "showDetails" buttons, all divs with class .details opens and get the .is-visible-class, but I just want the closest div to the item to be displayed. For some reason I think this is pretty simple, but I can't make it work.
How can I achieve that?
try this
<template>
<ul>
<li v-for="(item, i) in items" :key="item.id">
<div>
<p>{{item.text}}</p>
<button #click="showDetails(i)">Show details</button>
<div class="details" :class="i == active ? activeClass : 'hidden'">Some hidden details</div>
</div>
</li>
</ul>
</template>
<script>
export default {
data() {
return {
items: [],
activeClass: 'is-visible',
active: null
};
},
methods: {
showDetails(i) {
this.active = i;
}
}
};
</script>
It would be clearer to create a new component for list item which would contain all logic itself. Something like:
// ListItem.vue
<template>
<div>
<p>{{text}}</p>
<button #click="toggleVisibility">Show details</button>
<div class="details" v-show="isVisible">Some hidden details</div>
</div>
</template>
<script>
props: {
text: String
},
data() {
return {
isVisible: false
}
},
methods: {
toggleVisibility() {
this.isVisible = !this.isVisible
}
}
</script>
and in your parent component:
<ul>
<li v-for="item in items" :text="item.text" :key="item.id" is="list-item" /></li>
</ul>
data() {
return {
items: [ // a bunch of item objects here]
}
}
Just store "isVisible" variable inside the "item"
<ul>
<li v-for="item in items" :key="item.id">
<div>
<p>{{item.text}}</p>
<button #click="showDetails(item)">Show details</div>
<div class="details" :class="item.isVisible ? activeClass : 'hidden'">Some hidden details</div>
</div>
</li>
</ul>
data() {
return {
items: [ // a bunch of item objects here]
isVisible: false,
activeClass: 'is-visible'
}
},
methods: {
showDetails(item) {
item.isVisible = !item.isVisible;
this.$forceUpdate();
}
}

Why isn't the v-bind attribute working properly?

So I'm creating a simple To-Do List app using VueJS:
<template>
<div>
<br/>
<div id="centre">
<div id="myDIV" class="header">
<h2 style="margin:5px">My To Do List</h2>
<input type="text" id="myInput" v-model="text" v-on:keyup.enter="AddNote()" placeholder="Title...">
<span v-on:click="AddNote()" class="addBtn">Add</span>
</div>
<ul id="myUL">
<li v-on:click="ToggleClass(index)" v-for="(item, index) in array" v-bind:class="{ checked: isChecked[index] }">
{{item}}
<span class="close">×</span>
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: "Notepad",
data() {
return {
array: [],
text: "",
isChecked: []
}
},
methods: {
AddNote: function() {
if(this.text!=="") {
this.array.push(this.text);
this.isChecked.push(false);
this.text = "";
}
},
ToggleClass(index) {
console.log(index);
this.isChecked[index]=!this.isChecked[index];
console.log(this.isChecked);
}
}
}
</script>
However when I click on an item the v-bind attribute doesn't bind the class when I click on it. Instead it binds it when I type something in the text field above.
Can anyone please help?
The isChecked array is not reactive and vue cannot detect changes.
You have to trigger it, for example via $set or splice.
Read more about it here: https://v2.vuejs.org/v2/guide/list.html#Caveats
You can change your code like this:
ToggleClass(index) {
console.log(index);
this.isChecked.splice(index, 1, !this.isChecked[index])
// or this.$set(this.isChecked, index, !this.isChecked[index])
console.log(this.isChecked);
}

Array Splice always delete an item from last?

I am facing a problem in deleting item from an array. Array splice supposed to work but its not working like I want. Its always delete the item from last. I am using Vue.js . I am pushing item dynamically to an array. But after click remove its delete from the last. why I am facing this. I am attaching the codes.
<template>
<div>
<span class="badge badge-pill mb-10 px-10 py-5 btn-add" :class="btnClass" #click="addBtn"><i class="fa fa-plus mr-5"></i>Button</span>
<div class="block-content block-content-full block-content-sm bg-body-light font-size-sm" v-if="buttons.length > 0">
<div v-for="(item, index) in buttons">
<div class="field-button">
<div class="delete_btn"><i #click="remove(index)" class="fa fa-trash-o"></i></div>
<flow-button v-model="item.title" :showLabel="false" className="btn btn-block min-width-125 mb-10 btn-border" mainWrapperClass="mb-0" wrapperClass="pt-0" placeholder="Button Title"></flow-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import flowButton from '../assets/flow-button'
export default {
name: "textArea",
props:{
index : Number
},
data() {
return {
buttons : [],
btnClass : 'badge-primary',
}
}
components : {
flowButton
},
methods : {
addBtn () {
if(this.buttons.length >= 2) {
this.btnClass = 'btn-secondary'
}
if(this.buttons.length < 3) {
this.buttons.push({
title : ''
});
}
},
remove(index) {
this.buttons.splice(index, 1)
}
}
}
</script>
This must be because of your flow-button I have tried to replicate your error but endup to this code. I just replaced the flow-button with input and it works. Try the code below.
Use v-bind:key="index", When Vue is updating a list of elements rendered with v-for, by default it uses an “in-place patch” strategy. If the order of the data items has changed, instead of moving the DOM elements to match the order of the items, Vue will patch each element in-place and make sure it reflects what should be rendered at that particular index. This is similar to the behavior of track-by="$index"
You have missing comma between data and components, I remove the component here it won't cause any error now, and more tips don't mixed double quotes with single qoutes.
<template>
<div>
<span class="badge badge-pill mb-10 px-10 py-5 btn-add" :class="btnClass" #click="addBtn"><i class="fa fa-plus mr-5"></i>Button</span>
<div class="block-content block-content-full block-content-sm bg-body-light font-size-sm" v-if="buttons.length > 0">
<div v-for="(item, index) in buttons" v-bind:key="index">
<div class="field-button">
<div class="delete_btn"><i #click="remove(index)" class="fa fa-trash-o">sdfsdff</i></div>
<input type="text" v-model="item.title" :showLabel="false" className="btn btn-block min-width-125 mb-10 btn-border" mainWrapperClass="mb-0" wrapperClass="pt-0" placeholder="Button Title"/>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'textArea',
props: {
index: Number
},
data () {
return {
buttons: [],
btnClass: 'badge-primary'
}
},
methods: {
addBtn () {
if (this.buttons.length >= 2) {
this.btnClass = 'btn-secondary'
}
if (this.buttons.length < 3) {
this.buttons.push({
title: ''
})
}
},
remove (index) {
this.buttons.splice(index, 1)
}
}
}
</script>
I think that you may be facing a conflict with the index prop of your component. Try to use a different name for the index of your v-for loop:
<div v-for="(item, ind) in buttons">
<div class="field-button">
<div class="delete_btn"><i #click="remove(ind)" class="fa fa-trash-o"></i></div>
<flow-button v-model="item.title" :showLabel="false" className="btn btn-block min-width-125 mb-10 btn-border" mainWrapperClass="mb-0" wrapperClass="pt-0" placeholder="Button Title"></flow-button>
</div>
</div>
Try this. Removing an item correctly using this.
<div v-for="(item, ind) in buttons" :key="JSON.stringify(item)">

Categories

Resources