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

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.

Related

Vuejs SearchBar ||using filter method not working

I quite new to Vue js I am trying to use the computed method to create a search bar to only search through the name but I'm getting "this.info.filter" is not a function
<template>
<div class="container">
<input type="text" v-model="search" placeholder="Search by name">
<div class="content" v-for="student in filterName " v-bind:key="student.id">
<img class="image" :src="student.pic" alt="">
<div class="student-info">
<h1 class="info">{{student.firstName +" "+ student.lastName}}</h1>
<div class="infomation">
<p class="cop">{{ student.company }}</p>
<p class="ski">{{ student.skill }}</p>
<p class="email">{{ student.email }}</p>
<p class="grade">{{ student.grades }}</p>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "Student.vue",
data() {
return {
students: '',
search: ''
}
},
mounted() {
axios
.get('https://api.hatchways.io/assessment/students')
.then((res) => {
this.students = (res.data.students)
})
},
computed :{
filterName:function (){
return this.info.filter((student)=>{
return student.company.matcth(this.search);
})
}
}
}
</script>
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
First time using StackOverflow too please ignore the errors
I don't see a declaration/initialization for this.info anywhere in your code.
The reason you're getting that error is because filter is trying to run on an undefined variable (this.info is undefined).
You might want to initialize that to an empty array within data.

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>

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);
}

How do you implement the "double click to edit todo task" in VueJS?

I've been making a simple To Do app with VueJS. I have the add new todo, delete todo and mark as done functionalities done, but I'm struggling with the "Double Click to edit a task" feature.
I've added an input field which should appear when the user double clicks on the task to edit it but nothing seems to happen? Any help would be awesome :)
App.vue:
<template>
<div id="app">
<div class="container">
<div class="row">
<h1>VueJS To Do Manager:</h1>
</div>
</div>
<div class="container">
<div class="row">
<input class="new-todo input-group col-xs-12"
placeholder="Enter a task and press enter. Use the checkbox to mark them as done."
v-model="newTodo"
#keyup.enter="addTodo">
</div>
</div>
<TodoCard v-for="(todo, key) in todos"
:todo="todo"
:key="key"
#remove="removeTodo(key)"/>
</div>
</template>
<script>
import TodoCard from './components/TodoCard'
export default {
data () {
return {
todos: [],
newTodo: ''
}
},
components: {
TodoCard
},
methods: {
addTodo: function () {
// Store the input value in a variable
let inputValue = this.newTodo && this.newTodo.trim()
// Check to see if inputed value was entered
if (!inputValue) {
return
}
// Add the new task to the todos array
this.todos.push(
{
text: inputValue,
done: false
}
)
// Set input field to empty
this.newTodo = ''
},
removeTodo: function (key) {
this.todos.splice(key, 1)
}
}
}
</script>
TodoCard.vue component:
<template>
<div id="todo">
<div class="container">
<div class="row">
<input class="check" type="checkbox" />
<h3 class="col strikethrough"
#dblclick="editTodo(todo)">{{ todo.text }}</h3>
<div v-show="todo.edit == false">
<input v-show="todo.edit == true"
v-model="todo.title"
v-on:blur="todo.edit=false; $emit('update')"
#keyup.enter="todo.edit=false; $emit('update')">
</div>
<hr>
<button #click="removeTodo"
type="button"
class="btn btn-danger btn-sm">Delete</button>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['todo'],
methods: {
removeTodo: function (todo) {
this.$emit('remove')
},
editTodo: function (todo) {
this.editedTodo = todo
}
}
}
</script>
I think you don't set todo.edit to true when double click todo description. Moreover the div that contains the todo edit input has v-show="todo.edit == false" while it should be v-show="todo.edit == true" or just v-show="todo.edit" if you are sure that todo.edit is always a boolean.

Vuejs - Accordion

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>

Categories

Resources