Vuejs bind custom event on nested component within for loop - javascript

I'm using three single-file-components.
ParentVariation.vue
VariationInfo.vue
Childvariation.vue
I'm emitting MarkedFilled event from child component variation-info, and catching that event on ParentVariation. Here's the content of ParentVariation.vue :
<template>
<section class="parentVariation">
<label :for="'key-'+row.id">Key</label>
<select :name="'key-'+row.id" :id="'key-'+row.id" class="select2"></select>
<label :for="'value-'+row.id">Value</label>
<input :name="'value-'+row.id" :id="'value-'+row.id">
<label :for="'quantity-'+row.id">quantity</label>
<input :name="'quantity-'+row.id" :id="'quantity-'+row.id">
<variation-info :filled="row.filled" #markedFilled="row.filled='true'" :key="row.id"></variation-info>
<button #click="addChild" type="button" class="btn btn-link btn-sm btn-fw">
<i class="mdi mdi-table-column-plus-after"></i>
Add Child
</button>
<button #click="popChild" type="button" class="btn btn-link text-danger btn-sm btn-fw">
<i class="mdi mdi-table-column-remove"></i>
Drop Child
</button>
<br>
<div v-if="row.child.length > 0">
<child-variation v-for="child in row.child" :childIndex="child.id" :parentIndex="row.id" :key="child.id"></child-variation>
</div>
</section>
</template>
<script>
export default {
props: [ 'row' ],
methods: {
addChild() {
this.row.child.push({ id:this.row.child.length, filled:'' })
},
popChild() {
this.row.child.pop()
},
}
}
</script>
On the Main Vue-Instance. i have this:
data: function() {
return {
parents: [{ id:0, child: [], filled:'' }]
}
},
And I've initialized ParentVariation like so:
<parent-variation v-for="parent in parents" :row="parent" :key="parent.id"></parent-variation>
i'm trying to achieve this functionality: When MarkedFilled event is called from child component (variation-info). Parent component (parent-variation) will catch that & update filled property for data parent on main vue instance.
But each time this event is called, only first parent element's filled property is changed. I want to change the property of the clicked element.
I've been trying to figure this out for 2 days now, Any help is appreciated.
I just want to understand why only first element is called each time.

You might want to try emitting markedFilled event to parent component:
In ParentVariation.vue
<variation-info :filled="row.filled" #markedFilled="$emit('markedFilled')" :key="row.id">
</variation-info>
and in main
<parent-variation v-for="(parent, index) in parents" :row="parent" :key="parent.id" #markedFilled="changeFilled(index)">
</parent-variation>
export default {
data: function() {
return {
parents: [{ id:0, child: [], filled:'' }]
}
},
methods: {
changeFilled(index) {
this.parents[index].filled = true
this.parents = JSON.parse(JSON.stringify(this.parents))
}
}
}

Related

Why computed property works on preserving the reactivity but attributes in data() {} does not

I'm following this tutorial, where they point out a small bug with the checkbox status of forgetting the state of the ticked/unticked which can be solved by using the computed property.
I would like to ask, even if the attribute isDone in Data (ToDoItem.vue) has been changed to true (by ticking the checkbox), why the box is still unticked after clicking edit then cancel, and why computed property could solve this bug.
Below are parts of the scripts.
ToDoItem.vue
<template>
<div class="stack-small" v-if="!isEditing">
<div class="custom-checkbox">
<input
type="checkbox"
class="checkbox"
:id="id"
:checked="isDone"
#change="$emit('checkbox-changed')"
/>
<label :for="id" class="checkbox-label">{{ label }}</label>
</div>
<div class="btn-group">
<button
type="button"
class="btn"
ref="editButton"
#click="toggleToItemEditForm"
>
Edit <span class="visually-hidden">{{ label }}</span>
</button>
<button type="button" class="btn btn__danger" #click="deleteToDo">
Delete <span class="visually-hidden">{{ label }}</span>
</button>
</div>
</div>
<to-do-item-edit-form
v-else
:id="id"
:label="label"
#item-edited="itemEdited"
#edit-cancelled="editCancelled"
></to-do-item-edit-form>
</template>
<script>
import ToDoItemEditForm from "./ToDoItemEditForm";
export default {
components: {
ToDoItemEditForm,
},
props: {
label: { required: true, type: String },
done: { default: false, type: Boolean },
id: { required: true, type: String },
},
data() {
return {
isEditing: false,
isDone: this.done, // after deleting this line and use
//computed: {} below, the bug is solved.
};
},
// computed: {
// isDone() {
// return this.done;
// },
// },
};
</script>
ToDoItem.vue
<template>
<div id="app">
<h1>To-Do List</h1>
<to-do-form #todo-added="addToDo"></to-do-form>
<h2 id="list-summary" ref="listSummary" tabindex="-1"> {{ listSummary }} </h2>
<ul aria-labelledby="list-summary" class="stack-large">
<li v-for="item in ToDoItems" :key="item.id">
<to-do-item
:label="item.label"
:done="item.done"
:id="item.id"
#checkbox-changed="updateDoneStatus(item.id)"
#item-deleted="deleteToDo(item.id)"
#item-edited="editToDo(item.id, $event)"
>
</to-do-item>
</li>
</ul>
</div>
</template>
<script>
import ToDoItem from "./components/ToDoItem.vue";
import ToDoForm from "./components/ToDoForm.vue";
import uniqueId from "lodash.uniqueid";
export default {
name: "app",
components: {
ToDoItem,
ToDoForm,
},
data() {
return {
ToDoItems: [],
};
},
methods: {
updateDoneStatus(toDoId) {
const toDoToUpdate = this.ToDoItems.find((item) => item.id === toDoId);
toDoToUpdate.done = !toDoToUpdate.done;
console.dir(toDoToUpdate.done)
},
};
</script>
I'm not an expert in vue, but I believe that the this.done being assigned to isDone: is only done once in data(), and it wouldn't be done if the props change (the value of isDone in data() won't change when the prop done changes). While in computed, isDone will watch the done prop value, and if that prop value changes, the computed will be notified and thus changes the isDone data.

Am having issue implementing the javascript code with vue

Am very new to vuejs.am trying to replicate the javascript with vue. where a user can toggle button. I have a list of buttons and I would like to toggle the active class but remove the active class from all other buttons.. Is there a better way of writtting the function without the querySelector? Am really stuck..
<template>
<div #click="selectItem" class="menu-tabs">
<button type="btn" class="menu-tab-item active" data-target="#remis-transfer"> Transfer
</button>
<button type="btn" class="menu-tab-item text-muted" data-target="#bank-transfer">
Transfer Money
</button>
<button type="btn" class="menu-tab-item text-muted" data-target="#fueling">
Fueling
</button>
</div>
</template>
<script>
export default {
methods: {
selectItem(e) {
if (e.target.classList.contains("menu-tab-item") && !e.target.classList.contains("active")) {
const target = e.target.getAttribute("data-target")
menuTabs.querySelector(".active").classList.remove("active");
e.target.classList.add("active");
const menuSection = document.querySelector(".menu-section");
menuSection.querySelector(".menu-tab-content.active").classList.remove("active");
menuSection.querySelector(target).classList.add('active');
}
}
}
}
</script>
Look at the code
var Main = {
data() {
return {
active: 0,
buttonList: [
{
text: "Transfer",
target: "#remis-transfer",
},
{
text: "Transfer Money",
target: "#bank-transfer",
},
{
text: "Fueling",
target: "#fueling",
},
],
};
},
methods: {
selectItem(i) {
this.active = i;
},
},
};
var Ctor = Vue.extend(Main)
new Ctor().$mount('#app')
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.9/vue.js"></script>
<div id="app">
<div class="menu-tabs">
<button
type="btn"
class="menu-tab-item"
v-for="(item, index) in buttonList"
:class="[{ active: active == index }, { 'text-muted': active != index }]"
:data-target="item.target"
:key="index"
#click="selectItem(index)"
>
{{ item.text }}
</button>
</div>
</div>
You can simply define some states like:
data() {
return {
myActiveClass: 'active',
myMutedClass: 'text-muted'
}
},
and pass this myActiveClass and myMutedClass states to your elements like this:
class=menu-tab-item ${myMutedClass} ${myActiveClass}
with this approach, you can quickly achieve what you want. So when you want an element to not be active, in your function you can make myActiveClass = '', or if you want the text to lose muted class you can say myMutedClass = '' .
Just make sure to play with states in the way you want.

how to bind value of v-for to v-if

I'm working with BootstrapVue. To my problem: I have a v-for in my template in which I have two buttons.
Looping over my v-for my v-if doesn't generate unique IDs and than after clicking one button each button will be triggered (from Open me! to Close me! and other way around).
How can I manage to get each button only triggers itself and doesn't affect the other?
I think I have to use my n of my v-for but I actually don't know how to bind this to a v-if..
Thanks in advance!
<template>
<div>
<div v-for="n in inputs" :key="n.id">
<b-button v-if="hide" #click="open()">Open me!</b-button>
<b-button v-if="!hide" #click="close()">Close me! </b-button>
</div>
<div>
<b-button #click="addInput">Add Input</b-button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
id: null,
inputs: [{
id: 0
}],
hide: true,
};
},
methods: {
open() {
this.hide = false
},
close() {
this.hide = true
},
addInput() {
this.inputs.push({
id: this.id += 1;
})
}
}
};
</script>
Everything seems to look fine. In order to handle each button triggers,
you can maintain an object like so:
<script>
export default {
data() {
return {
inputs: [{id: 0, visible: false}],
};
},
methods: {
open(index) {
this.inputs[index].visible = false
},
close(index) {
this.inputs[index].visible = true
},
addInput() {
this.inputs.push({id: this.inputs.length, visible: false});
}
}
};
</script>
and your template should be like
<template>
<div>
<div v-for="(val, index) in inputs" :key="val.id">
<b-button v-if="val.visible" #click="open(index)">Open me!</b-button>
<b-button v-if="!val.visible" #click="close(index)">Close me! </b-button>
</div>
</div>
</template>
Edit:
You don't need to insert an id every time you create a row, instead can use the key as id. Note that the inputs is an object and not array so that even if you want to delete a row, you can just pass the index and get it removed.
I would create an array of objects. Use a boolean as property to show or hide the clicked item.
var app = new Vue({
el: '#app',
data: {
buttons: []
},
created () {
this.createButtons()
this.addPropertyToButtons()
},
methods: {
createButtons() {
// Let's just create buttons with an id
for (var i = 0; i < 10; i++) {
this.buttons.push({id: i})
}
},
addPropertyToButtons() {
// This method add a new property to buttons AFTER its generated
this.buttons.forEach(button => button.show = true)
},
toggleButton(button) {
if (button.show) {
button.show = false
} else {
button.show = true
}
// We are changing the object after it's been loaded, so we need to update ourselves
app.$forceUpdate();
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<template>
<div>
<div v-for="button in buttons" :key="button.id">
<button v-if="button.show" #click="toggleButton(button)">Open me!</button>
<button v-if="!button.show" #click="toggleButton(button)">Close me! </button>
</div>
</div>
</template>
</div>

How to use v-for with vuex to render forms if I need v-bind?

I have the following UI:
I will try to explain what is going on here. I have "Add message" button. When I click on the button I have new form with message: title, body, image, language (just multiple select via this plugin). I have clicked twice - I have 2 messages. Simple.
I don't use vue router. Implementation of my routing is with the help of backend. It means that for each route I have new vuex state.
I'm going to keep my messages in vuex, but it's impossible to use v-model for this case.
So, I will show my code.
store:
export const store = new Vuex.Store({
state: {
messages: [],
// more props are here ...
},
mutations: {
setMessages(state, messages) {
state.messages = messages;
},
// more setters are here
},
getters: {
getMessages: state => {
return state.messages;
},
// more getters are here
},
actions: {
updateMessagesAction: function({commit}, value) {
console.log(value)
},
}
});
Messages component:
<template>
<div>
<button class="btn btn-outline-info" #click.prevent="createNewMessage">
<i class="fa fa-language"/> Add message
</button>
<div>
// now it works with local state, but I need to work with vuex
<div v-for="(message, index) in messages">
<button class="btn btm-sm btn-danger" #click="deleteMessage(index, message)"><i class="fa fa-remove"/>
</button>
<b-collapse collapsed :id="`collapse-${index}`">
<form>
<div class="form-group">
<label class="typo__label">Languages</label>
<multiselect
v-model="message.languages"
:options="getLanguagesOptions"
:multiple="true"
:close-on-select="true"
:clear-on-select="false"
:preserve-search="true"
placeholder="Languages"
label="name"
track-by="id"
>
</multiselect>
</div>
<div class="form-group">
<label for="title" class="typo__label">Title</label>
<input type="text" id="title" class="form-control" autocomplete="off" ??? how to bind it to vuex ???? I dont understand :(((>
</div>
<div class="form-group">
<label for="text" class="typo__label">Body</label>
<textarea class="form-control" id="text" ??? how to bind it to vuex ???? I dont understand :(((/>
</div>
<div class="form-group">
<div id="upload-image">
<div v-if="!message.imageSrc">
<h2>Image</h2>
<input type="file" ref="file" #change="onFileChange($event, message)">
</div>
<div v-else>
<img :src="message.imageSrc"/>
<button #click.prevent="removeImage($event, message)">Remove</button>
</div>
</div>
</div>
<hr class="mb-2">
</form>
</b-collapse>
</div>
</div>
</div>
</template>
<script>
// imports
export default {
async created() {
// set languages from servert to vuex
let res = (await axios.post(this.urlForGettingLanguagesFromServerProp)).data;
this.$store.commit('setLanguagesOptions', res);
},
name: "MessagesComponent",
props: {
urlForGettingLanguagesFromServerProp: String,
uploadImageUrl: String,
deleteImageUrl: String,
selectedLanguagesIdsProp: {
type: Array,
default: () => []
},
},
methods: {
...mapMutations(['setLanguagesSelected']),
...mapActions(['updateMessagesAction']),
createNewMessage: function () {
let message = {
languages: [],
languagesIds: [],
title: "",
text: "",
imageSrc: "",
imageDbId: 0
};
this.messages.push(message);
},
deleteMessage: function (index, message) {
this.removeImage("", message);
this.messages.splice(index, 1);
},
onFileChange: async function (e, message) {
// this method add send image on server and save to state db image id ant path
},
removeImage: function (event, message) {
// remove image from server
}
},
computed: {
...mapGetters(['getLanguagesOptions', 'getMessages'])
},
watch: {
messages: {
deep: true,
immediate: true,
handler(val, oldVal) {
let message = JSON.parse(JSON.stringify(val));
this.$store.commit("setMessages", message);
}
}
},
data() {
return {
messages: [],
}
}
}
</script>
I call this component in a parent component. In the parent component I initiate vuex during update operation.
As you can see this component works with local state and synchronize local state with vuex. It's ok for Create operation. I can send messages to vuex, then I can take it in the parent component with other information and send it on the server. But what to do with Update? I have data from the server in the parent component but local state, of course, is empty for the Messages component. How to bind all messages to vuex and have ability to change each message separately? I mean, for example, to change title of the first message and to have it in vuex immediately?
Please, help me improve this component.

Updating an input's bound value when edited

I'm working on a basic to-do application. Each to-do/task item gets listed as an input item in a Vue <list-item> component, and the <list-item>s are displayed with a v-for pointing to a tasks array.
I'm trying to allow the user to edit each task input, and upon changing the value, have this update the array item (rather than just the input itself). My #change event on the input is firing, but I'm at a loss as to what to do after this point.
https://jsfiddle.net/xbxm7hph/
HTML:
<div class="app">
<div class="add-control-area columns is-mobile is-multiline">
<responsive-container>
<div class="field is-grouped">
<div class="control is-expanded">
<input class="input add-control-text" type="text" placeholder="New Task" v-model="newTask" v-on:keyup.enter="addTask">
</div>
<div class="control">
<a class="button is-white add-control-button" #click="addTask" :disabled="!isThereText">Add Task</a>
</div>
</div>
</responsive-container>
<responsive-container>
<list-item v-for="task, index in tasks" :item="task" :index="index" #task-completed="completeTask(index)" #task-deleted="deleteTask(index)" ></list-item>
</responsive-container>
</div>
</div>
JS:
Vue.component('list-item', {
props: ['item', 'index'],
template: `<div class="task-wrapper">
<input class="task" :value="item" #change="updateTask()">
<div class="task-control delete-task" #click="deleteTask()"></div>
<div class="task-control complete-task" #click="completeTask()"></div>
</div>
`,
methods: {
completeTask: function() {
this.$emit('task-completed', this.index);
},
deleteTask: function() {
this.$emit('task-deleted', this.index);
},
updateTask: function() {
console.log('changed');
}
}
});
Vue.component('responsive-container', {
template: `
<div class="column is-4-desktop is-offset-4-desktop is-10-tablet is-offset-1-tablet is-10-mobile is-offset-1-mobile">
<div class="columns is-mobile">
<div class="column is-12">
<slot></slot>
</div>
</div>
</div>
`
});
var app = new Vue({
el: '.app',
data: {
tasks: [],
completedTasks: [],
newTask: ''
},
methods: {
addTask: function() {
if(this.isThereText) {
this.tasks.push(this.newTask);
this.newTask = '';
this.updateStorage();
}
},
completeTask: function(index) {
this.completedTasks.push(this.tasks[index]);
this.tasks.splice(index, 1);
this.updateStorage();
},
deleteTask: function(index) {
this.tasks.splice(index, 1);
this.updateStorage();
},
updateStorage: function() {
localStorage.setItem("tasks", JSON.stringify(this.tasks));
}
},
computed: {
isThereText: function() {
return this.newTask.trim().length;
}
},
// If there's already tasks stored in localStorage,
// populate the tasks array
mounted: function() {
if (localStorage.getItem("tasks")) {
this.tasks = JSON.parse(localStorage.getItem("tasks"));
}
}
});
Use a v-model directive on your <list-item> component, instead of passing in an item property. You will also need to pass in a reference from the array (tasks[index]), because task in this scope is a copy that is not bound to the element of the array:
<list-item v-for="task, index in tasks" v-model="tasks[index]"></list-item>
In your component definition for the list item, you'll need to now take in a value prop (this is what gets passed when using v-model) and set a data property item to that value. Then, emit an input event on the change to pass the item value (this is what the component is listening for when using v-model):
Vue.component('list-item', {
props: ['value'],
template: `<div class="task-wrapper">
<input class="task" v-model="item" #change="updateTask"></div>
</div>
`,
data() {
return {
item: this.value,
}
},
methods: {
updateTask: function() {
this.$emit('input', this.item);
}
}
});
Here's a fiddle with those changes.
As Bert Evans mentioned, even though this works, Vue requires that components using the v-for directive also use a key attribute (you will get a warning from Vue otherwise):
<list-item
v-for="task, index in tasks"
:key="index"
v-model="tasks[index]"
></list-item>
Also, realize that the index variable in a v-for scope can change, meaning that the item at index 1 might change to index 4 and this can pose some problems as the application gets more complex. A better way would be to store items as an object with an id property. This way you can have an immutable id associated with the item.
You can pass the index and new value to your change event handler:
<input class="task" :value="item" #change="updateTask(index, $event)">
Then access them accordingly:
updateTask: function(index, event) {
console.log(index);
console.log(event.target.value);
}

Categories