How to trigger event in all sibling components except current component in vuejs? - vue.js

I have a reusable component that does inline editing for data.
So a page has 10 fields that can be edited inline,
<editfield :value="field" v-for="field in fieldslist"></editfield>
Each of them have a data field called "editing" that sets as true or false as the user clicks on it. Everytime a field is set to editing an event editing-another-field is emitted using event bus.
edit(){
this.editing = true;
EventBus.$emit('editing-another-field');
}
I added the event listener when the component is created
created(){
EventBus.$on('editing-another-field', ()=>{ this.editing = false;});
}
The problem I am facing is it is triggering the event even in the currennt component being edited.
How can I mention that updated value of editing in all the other sibling components except the current component.

Why not pass the current component as an event argument and use that to check if the event originated from this component or another one.
edit() {
this.editing = true;
EventBus.$emit('editing-another-field', this);
}
created() {
EventBus.$on('editing-another-field', source => {
if (source !== this) {
this.editing = false;
}
});
}
Or you can do it like this (it is important to unregister the event listener when the component is destroyed to avoid a memory leak):
edit() {
EventBus.$emit('editing-field', this);
}
created() {
this.editingFieldHandler = vm => {
this.editing = vm === this;
};
EventBus.$on('editing-field', this.editingFieldHandler);
}
destroyed() {
EventBus.$off('editing-field', this.editingFieldHandler);
}
Otherwise you can emit the event first and then set this.editing to true.

Are you sure you want an event bus? This brings up bad memories of JQuery ;-) I think it would be cleaner to limit yourself to a tree of parents and children. Thinking MVVM, formLockedBy is a perfectly valid and sensible property to store on the parent and pass to the children.
The solution below, running here, shows a form with two fields. The fields are both instances of modal-component. The parent manages the formLockedBy property. The child fields look at this property to know to disable themselves. When the user starts typing in a field, the field emits an editing event and formLockedBy gets set. Similarly, when a field emits a save or cancel event, the parent clears formLockedBy and the other input(s) spring back to life.
Note the advantages...
Only the parent listens for events.
The identifier stored in formLockedBy is just the string name of the field. This is much safer than passing and storing a reference to the Vue component. If you don't like this, you might consider adding a safe id to the object proto.
No surprises. The full list of events the parent will react to is declared in the tag instantiating the child. The child specifies in props everything it needs from the parent.
HTML
<div id="example">
<modal-input name='First Name'
:form-locked-by='this.formLockedBy'
v-on:save='formLockedBy = null'
v-on:cancel='formLockedBy = null'
v-on:editing='fieldActive'
></modal-input>
<modal-input name='Address'
:form-locked-by='this.formLockedBy'
v-on:save='formLockedBy = null'
v-on:cancel='formLockedBy = null'
v-on:editing='fieldActive'
></modal-input>
</div>
JS
Vue.component('modal-input', {
template: `<div>
{{name}} :
<input :name='name' type="text" v-on:keydown="active" :disabled="formLockedBy && formLockedBy != name"/>
<span v-if="editing && formLockedBy == name">
<input type="button" value="Save" v-on:click="$emit('save');editing=false;"></input>
<input type="button" value="Cancel" v-on:click="$emit('cancel');editing=false;"></input>
</span>
</div>`,
data : function(){
return {editing:false};
},
props: ['name','formLockedBy'],
methods : {
active : function(event){
if(!this.editing){
this.editing = true;
this.$emit('editing',{field:this.name})
}
return true;
}
}
});
// create a root instance
new Vue({
el: '#example',
data: {
formLockedBy : null
},
methods : {
fieldActive : function(args){
this.formLockedBy = args.field;
}
}
})

Related

v-model not always updating in Vue

Short question
The v-model which binds a string to an input field won't update in some cases.
Example
I am using Vue within a Laravel application. This is the main component which contains two other components:
<template>
<div>
<select-component
:items="items"
#selectedItem="updateSelectedItems"
/>
<basket-component
:selectedItems="selectedItems"
#clickedConfirm="confirm"
#clickedStopAll="stopAll"
/>
<form ref="chosenItemsForm" method="post">
<!-- Slot for CSRF token-->
<slot name="csrf-token"></slot>
<input type="text" name="chosenItems" v-model="selectedItemsPipedList" />
</form>
</div>
</template>
<script>
export default {
props: ["items"],
data: function() {
return {
selectedItems: [],
selectedItemsPipedList: ""
};
},
methods: {
updateSelectedItems: function(data) {
this.selectedItems = data;
this.selectedItemsPipedList = this.selectedItems
.map(item => item.id)
.join("|");
},
confirm() {
this.$refs.chosenItemsForm.submit();
},
stopAll() {
this.updateSelectedItems([]);
this.confirm();
}
}
};
</script>
The method updateSelectedItems is called from the select-component and it works fine. In the end, the selectedItemsPipedList contains the selected items from the select-component, which looks like "1|2|3" and this value is bound to the input field in the chosenItemsForm. When the method confirm is called from the basket-component, this form is posted to the Laravel backend and the post request contains the chosen items as piped list. So far, so good.
The method stopAll is called from the basket-component and it will remove all the selected items from the array. Therefore it will call the method updateSelectedItems with an empty array, which will clear the selectedItems array and then clear the selectedItemsPipedList. After that, confirm is called which will post the form again. But, the post value still contains the selected items (e.g. '1|2|3'), instead of "". It looks like the v-model in my form is not updated, which is strange because it does work when selecting items. Why is it working when adding items, and doesn't when removing all items?
I believe you have a timing issue here. The value of the properties haven't been propagated to the DOM yet, so the form submission is incorrect. Try this instead:
stopAll() {
this.updateSelectedItems([]);
//NextTick waits until after the next round of UI updates to execute the callback.
this.$nextTick(function() {this.confirm()});
}

Vue wrapper example

There are a good examples of integrating vue with select2.
I have a question.
If we look at this part of the code:
mounted: function () {
var vm = this
$(this.$el)
// init select2
.select2({ data: this.options })
.val(this.value)
.trigger('change')
// emit event on change.
.on('change', function () {
vm.$emit('input', this.value)
})
}
I don't understand, why when we change value of select2, this.value changes too.
I expected a record like:
.on('change', function () {
this.value = $(this.$el).val()
vm.$emit('input', this.value)
})
It behaves that way because of how v-model works. What you are a looking at is a 2-way binding. Where if the value of selected in v-model="selected" changes, the value will be pushed down to the component.
When vm.$emit('input', this.value) is called, it tells the parent to update whatever variable is listening to changes, in this case selected, which it turn gets pushed back to the component such that its value gets changed.
To make it simpler to understand, this is the sequence of events:
select2's value changes
the select2 value change triggers an event emission
the parent receives the event and updates selected
the component's value gets assigned to the new value of selected
the watcher for the value gets triggered and updates select2 with the new value
Good question though.
Caveat: Doing this is understandably poor practice. It will break it sad ways when used with 1-way bindings.
After writing my previous answer, I realized I missed something important: contexts.
On line 5 in the jsfiddle, there is this line:
var vm = this, why?
This is because later on, when doing vm.$emit, this.$emit will not work; the context has been changed.
.on('change', function () {
//this = select2 element
vm.value // == 1
this.value // == 2
vm.$emit("input", this.value);
})
The value on the emit event is not that of the component, but of the select2 element.
While the value has not yet been changed on the component, the new value has been broadcasted to the parent.
Notice how select2 component used in the template:
<select2 :options="options" v-model="selected">
<option disabled value="0">Select one</option>
</select2>
In Vue.js using v-model with components could be simplified to:
<your-component
:value="selected"
#input="value => { selected = value }">
</your-component>
So every time you emit an event from your component the value gets changed at the parent component.
Source

VueJS 2 - Listen to event in mixin

I'm currently trying to create a mixin for Vue which basically creates a property passthrough chain. I'll clarify what should happen to be a little more clear;
Let's say I got 3 components; A,B and C.
A & B are both the same component called 'content-pane' (See below for template code).
<div class="pane-wrapper">
<div class="content-pane" :class="{'is-hidden' : !active}" :content="name">
<div class="card white">
<div class="card-title grey darken-3">
<h1 class="white-text">{{ label }}</h1>
</div>
<div class="card-content white">
<component
:is = "type"
:routes = "routes"
:passthrough = "passthrough"
keep-alive
></component>
</div>
</div>
</div>
<content-pane
v-for="(pane, key) in children"
:key = "key"
:label = "pane.label"
:name = "pane.name"
:active = "true"
:type = "pane.type"
:routes = "pane.routes"
></content-pane>
</div>
C is a dynamic component, meaning that it is interchangeable and could be any component.
Now I want to be able to access certain data from component C in component A, and for that I am trying to create a mixin that dyamically offers a data property to do this:
<script>
export default {
name: 'passthrough',
props: {
passthrough : {
type : Object
}
},
data ()
{
return {
// This object allows you to
// update the parent.
passthroughModifier : {
// We use the data object inside the
// original object because Vue doesn't
// want to detect direct prop changes
// when they are added dynamically
// into the root object...
data : {}
}
}
},
methods : {
/**
* This function fires an emit event.
*/
emitUpdate ()
{
this.$emit('passthrough-update', this.passthroughModifier);
}
},
watch : {
/**
* Emit an event once the passthrough
* property has been changed.
* We need to use a deep watcher.
*/
'passthroughModifier' : {
handler : function (val) {
this.emitUpdate();
},
deep: true
}
},
created ()
{
// Allow access to the instance
// inside the iteration.
let _that = this;
// Attach a listener for the passthrough update
// which will walk through all the keys in the
// data object and hard-set these locally.
this.$on('passthrough-update', function (data) {
Object.keys(data).forEach(function (index) {
_that.passthroughModifier[index] = data[index];
});
});
}
}
Everything works fine except listening to the 'passthrough-update' event, which is fired by the watcher on $.passthroughModifier.
So; When component C updates its $.passthroughModifier.data, the event gets emitted, but component B isn't able to catch this event.
I have tried to listen for this event in the created() method of the mixin (see code above), but it seems as if the event only gets caught in the component the event is fired from. So component C fires the event, and component C listens to its own event.
I hope someone is able to tell me wether this is actually possible or not, and what I'm doing wrong if it is possible.

Can't copy props to model data and render it in Vue 2

I'm having this problem that looks a lot like a bug to me and I can't figure out how to solve it.
I created a generic list component and I tell it what child component it should insert in each item and what are the data it should pass to the child component. I'm passing everything as props along with the list (array) itself.
The problem is that I can't mutate the list props. So I try to copy it to model attribute. Otherwise I get this error:
Avoid mutating a prop directly since the value will be overwritten
whenever the parent component re-renders.....
And I can't just make it work in any of the lifecycle events. When I save the file and the hot-reloading reloads the page, the list is there, rendered, full of items. When I press F5 to manually reload the page, it is no more. Everything seems to be alright with code though
So in the parent component I'm doing this:
<List ref="link_list"
:list="this.foo.links" //this is array
:child="'LinkFormItem'" //this is the name of the child component
:section_name="'Links'"
:defaults="{content: '', type: 'facebook'}" />
In the List component I get this:
Template
<li class="" v-for="item in datalist">
<component :is="child" :item="item" ></component>
<button v-on:click='remove(index++)' type="button" name="button" class='red button postfix small'>Remove</button>
</li>
Script
<script>
import Child1 from './Child1'
import Child2 from './Child2'
export default {
name: 'search',
props: ['child', 'list', 'defaults','section_name'], //it is received as 'list'
components: {
Child1, Child2
},
data () {
return {
index: 0,
datalist: [] //i'm trying to copy 'list' to 'datalist'
}
},
beforeMount: function () {
// i'm copying it
for(var k in this.list){
this.datalist.push(this.list[k])
}
},
methods: {
//and here I should change it the way I want
add: function () {
this.datalist.push(this.defaults)
},
getList () {
return this.datalist;
},
remove(index){
var datalist = [];
for(var k in this.datalist){
if(k != index) datalist.push(this.datalist[k]);
}
this.datalist = datalist;
}
}
}
</script>
I don't see any problems with my Script. What is going on??
#edit
Ok, some console.log later I found out what the problem seems to be. The HTTP Request is really taking much longer than the mounting of the component to happen. But when it happens, it is not triggering the update in the list component. Nothing is re-rendered and the list is empty.
Workaround
well I realised the problem was related to propagation. I made a few changes in the code to asure the parent component was updating and changing the model value. but the child component (the list component) was not receiving it.
then I gave up trying to understand why and did the following:
1- used the ref in the child component to force an update in the child component with $forceUpdate and then I was assigning the props to the model in the beforeUpdate event. It was causing an error: an re-rendering loop. The update caused a new update and so on. We could just use a flag to stop it.
2- Instead I just called a child method directly:
this.$refs.link_list.updateList(data.links);
I hate this approach because I think it's way too explicit. But it did the job. Then in the child component a new method:
updateList(list){
this.datalist = list;
}
3- The other possibility that passed through my mind was emitting an event. But I didn't try, too complicated
You can simply do like as follows
data () {
return {
index: 0,
datalist: this.list // to copy props to internal component data
}
},
Once you done about you need to apply data manipulation opertions on new this.datalist , not on this.list
If you don't want to mutate the original list array you can do this:
data () {
return {
index: 0,
datalist: Object.assign({}, this.list)
}
}
I think this will help you

vue custom emit doesnt work in v-for

I have a component that looks like this:
<task-list :content="lesson.children" :iterator="lesson.section" :user="user" #updateTask="updateTask"></task-list>
I use component in 2 different pages. In on page its inserted just as is, in another page its parent is a v-for so it looks like:
<div class="lesson-list-item" v-bind:class="{ 'lesson-complete': course.iterator_position > index}" v-for="(lesson, index) in content" >
<task-list :content="lesson.children" :iterator="lesson.section" :user="user" #updateTask="updateTask"></task-list>
</div>
Both pages have the method:
updateTask(index){
var self = this;
this.loaded = false;
this.iterator = index;
this.newTask = index;
setTimeout(function() { self.loaded = true }, 10);
}
This is what task-list emits on a click:
cardClick(index, slug, initialIterator){
console.log(index);
if( index <= this.initialIterator){
this.$emit('updateTask', index);
}
}
The page where its not wrapped in a v-for works, the one where it is never receives the emit event.
I think your component doesn't emit an updateTask event.
Add this to the method that you want to fire the event: this.$emit('updateTask');