How to amend data in a different component? - vue.js

I am new to Vue js and trying to build a simple CRUD example for myself.
Following the documentation on non parent child communication I would like to amend the heading value in the data of one component but from another component.
I set up a fiddle to showcase the relative functionality as I currently understand it and we have here the HTML:
<div id="app" v-cloak>
<person-add></person-add>
<person-list :list="people"></person-list>
</div>
<template id="person-add-template">
<div>
<h2>
<span>{{ heading }}</span>
Person
</h2>
<form #submit.prevent="handleFormSubmit">
<input type="text" placeholder="Enter persons name" v-model="name" />
<button type="submit" v-show="name">
Add Person
</button>
</form>
</div>
</template>
<template id="person-list-template">
<div>
<h2>People</h2>
<table border="1">
<tr>
<th>Person</th>
<th>Edit</th>
</tr>
<tr v-for="(person, key) in list">
<td>{{ person.name }}</td>
<td><button type="button" #click="editPerson(key)">Edit</button></td>
</tr>
</table>
</div>
</template>
And the JS:
// https://v2.vuejs.org/v2/guide/components.html#Non-Parent-Child-Communication
var bus = new Vue();
// Add
Vue.component('person-add', {
template: '#person-add-template',
props: ['list'],
data: function () {
return {
heading: 'Add',
name: ''
}
},
created: function () {
bus.$on('toggle-heading', function (newHeading) {
console.log(newHeading);
this.heading = newHeading;
});
}
});
// List
Vue.component('person-list', {
template: '#person-list-template',
props: ['list'],
methods: {
editPerson: function (key) {
console.log('fired');
bus.$emit('toggle-heading', 'Edit');
}
}
});
// Vue
new Vue({
el: '#app',
data: {
people: [
{ name: 'Bob' },
{ name: 'Frank' },
{ name: 'Mary' }
]
}
});
As you can see, it presents a simple form that starts with "Add Person" and lists some people along with an edit button for each:
What I would like to happen is that when I click on edit next to a persons name then it will change the heading in the other component to say "Edit Person" as opposed to the default "Add Person".
In the method in component A I have:
editPerson: function (key) {
console.log('fired');
bus.$emit('toggle-heading', 'Edit');
}
And in the created hook within component B I have:
created: function () {
bus.$on('toggle-heading', function (newHeading) {
console.log(newHeading);
this.heading = newHeading;
});
}
When I click edit, in the console I see the logs fired and then Edit so the event seems to follow through to the person-add component but where I have tried to assign the new heading this.heading = newHeading;, the heading does not change and I am battling to understand why.
If anyone could suggest why this is happening, where I have gone wrong with this or how things should be done if this is not the right way then it would be greatly appreciated.
Many thanks in advance!

Your problem is actually to do with scope, not a lack of understanding of Vue. Your code is correct, except you are trying to access this from inside a function creating it's own this context.
Whenever you create a new function this way, it creates it's own this so when you do:
bus.$on('toggle-heading', function(newHeading) {
console.log(newHeading);
// this refers to this anonymous function only
this.heading = newHeading;
});
this only refers to the function itself, not the the Vue instance.
The way to get around this is to use an arrow function, which do not create their own this:
bus.$on('toggle-heading', (newHeading) => {
console.log(newHeading);
// No new 'this' context is created in an arrow function
this.heading = newHeading;
});
Or if you are not using ECMAScript 2015 you will need to set a reference to this outside the function:
var self = this; // set a reference to "this"
bus.$on('toggle-heading', function(newHeading) {
console.log(newHeading);
// Now self refers to the view models `this`
self.heading = newHeading;
});
I've updated your fiddle to show you the two methods:
Arrow Function: https://jsfiddle.net/abtgmx47/3/
Using var self=this reference: https://jsfiddle.net/abtgmx47/4/

Related

Vuejs :class not working as expected in template

The class binding is not working as expected using a template, as the image below shows:
I have a array with many categories and when the user click it has to filter, this i already did, for whose category he wants. My problem is that once i click in other category, the ones before stills on.
The template receives a array such as:
categories: ["todos", "beer", "eco-bag", "paper-bag", "suplementos", "chas", "doces", "chocolates", "dieteticos"]
Here is the template:
<template id="category-box">
<span :class="{active: currentFilter == category}" #click="setFilter(category)">
{{category}}
</span>
</template>
The call inside the #app element:
<div id="category">
<category v-for="category in categories" :category="category"></category>
</div>
The code that handles it:
const Category = Vue.component("category", {
template: "#category-box",
props: {
"category": String,
},
data: function() {
return {
currentFilter: "todos"
}
},
methods: {
setFilter: function(filter) {
this.currentFilter = filter;
this.$parent.$emit('filteredCategory', filter);
}
}
});
It looks like currentFilter is scoped to the individual Category component. Each time you set it you are setting it for that component. Move it to the parent scope so that there is only one currentFilter for all of your categories.

Conditional link behavior in VueJS

Couldn't find a proper name for the title, will be glad if someone figures out a better name.
I have a component which represents a product card. The whole component is wrapped in <router-link> which leads to product page.
However I have another case, when I do not need the component to lead to a product page, but instead I need to do some other action.
The only solution I found is to pass a callback function as a prop, and based on this, do something like:
<router-link v-if="!onClickCallback">
... here goes the whole component template ...
</router-link>
<div v-if="onClickCallback" #click="onClickCallback">
... here again goes the whole component template ...
</div>
How can I do this without copy-pasting the whole component? I tried to do this (real code sample):
<router-link class="clothing-item-card-preview"
:class="classes"
:style="previewStyle"
:to="{ name: 'clothingItem', params: { id: this.clothingItem.id }}"
v-on="{ click: onClick ? onClick : null }">
However I got this: Invalid handler for event "click": got null
Plus not sure if it's possible to pass prevent modificator for click and this just looks weird, there should be a better architectural solution
Commenting on the error, you could use an empty function instead of null, in the real code snippet
<router-link class="clothing-item-card-preview"
:class="classes"
:style="previewStyle"
:to="{ name: 'clothingItem', params: { id: this.clothingItem.id }}"
v-on="{ click: onClick ? onClick : null }">
This should works (replace a for "router-link" then insert right properties)
Further infos :
https://fr.vuejs.org/v2/guide/components-dynamic-async.html
v-bind is simply an Object where each keys is a props for your component, so here, I programmatically defined an object of properties depending on the wrapper (router link or a simple div). However we cannot do this for events (of course we could create our own event listener but it's a little bit tricky) so I simply but an handle method.
new Vue({
el: "#app",
data: {
products : [{onClickCallback : () => { alert("callback"); return true;}}, {}, {}]
},
methods : {
handleClick(product, event) {
if (!product.onClickCallback) return false
product.onClickCallback()
return true
},
getMyComponentName(product) {
if (product.onClickCallback) return "div"
return "a"
},
getMyComponentProperties(product) {
if (product.onClickCallback) return {is : "div"}
return {
is : "a",
href: "!#"
}
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<component
v-for="(product, index) in products"
:key="index"
v-bind="getMyComponentProperties(product)"
#click="handleClick(product, $event)"
>
<div class="product-card">
<div class="product-card-content">
<span v-show="product.onClickCallback">I'm a callback</span>
<span v-show="!product.onClickCallback">I'm a router link</span>
</div>
</div>
</component>
</div>
Do you have to use a <router-link>? If it can safely be a <div>, you could use something like
<div #click="handleClick" ...>
<!-- component template -->
</div>
and
methods: {
handleClick (event) {
if (this.onClickCallback) {
this.onClickCallback(event)
} else {
this.$router.push({ name: 'clothingItem', ... })
}
}
}
See https://router.vuejs.org/guide/essentials/navigation.html

Vue: Style list row when selected

I have a list of components called client-row and when I select one I want to change the style. I run into an issue when trying to remove the styling from the previously selected row, when selecting a new row.
Vue.component('client-row', {
template: '#client-row',
props: {
client: Object,
},
data: function() {
return {
selected: false
}
},
methods: {
select: function() {
// Does not work properly
el = document.querySelector('.chosen_row')
console.log(el)
if ( el ) {
el.className = el.className - "chosen_row"
}
this.selected = true
this.$emit('selected', this.client)
}
}
})
<script type="text/x-template" id="client-row">
<tr v-bind:class="{ 'chosen_row': selected }">
<td>{{ client.name }}</td>
<td>{{ client.location_name || 'no location found' }}</td>
<td>{{ client.email || 'no email found' }}</td>
<td><button class="btn btn-sm btn-awaken" #click="select()">Select</button></td>
</tr>
</script>
I can properly set the selected property, but cannot seem to remove it reliably.
It is generally bad practice to manually modify DOM elements in components. Instead, I recommend that you change the parent component to have a data field to keep track of which row is selected, and to pass that value into the rows. The row would then check whether its value matches the parent's selected row and apply style if true.
DOM manipulation in components is a sign you are doing things very wrong in vue.
In your case, vue and your manually DOM manipulation are battling each other. Vue is tracking whether to add the chosen_row class on the tr based on whether the child's data field selected is true or not. In your code, you are only ever setting it to true. Vue will always try to include the class for any row you have clicked. Then you are manually removing the class from all rows that were previously clicked, however Vue will still try to add the class because selected is still true in the child components that have been clicked.
You need to do a data oriented approach rather than a DOM manipulation based approach.
Child:
Vue.component('client-row', {
template: '#client-row',
props: {
client: Object,
selectedClient: Object
},
methods: {
select: function() {
this.$emit('selected', this.client);
}
}
})
<script type="text/x-template" id="client-row">
<tr v-bind:class="{ 'chosen_row': client === selectedClient }">
<!-- td's removed for brevity -->
<td><button class="btn btn-sm btn-awaken" #click="select">Select</button></td>
</tr>
</script>
Parent:
Vue.component('parent', {
template: '#parent',
data() {
return {
clients: [],
selectedClient: null
};
},
methods: {
clientSelected(client) {
this.selectedClient = client;
}
}
})
<script type="text/x-template" id="parent">
<!-- i dont know what your parent looks like, so this is as simple as i can make it -->
<div v-for="client in clients">
<client-row :client="client" :selected-client="selectedClient" #selected="clientSelected"></client-row>
</div>
</script>
In addition:
Your click event handler on the button can be shortened to #click="select" which is the recommended way of binding methods.

Array Reactivity Issues

I'm having issues with array reactivity, please see this example: https://jsfiddle.net/jk1kadxq/
var app = new Vue({
el: "#app",
data: function () {
return {
grid: {
rows: [{}]
}
}
},
methods: {
addRow: function () {
this.grid.rows.push({});
},
setRow: function (row) {
console.log(row);
this.$set(row, 'cell', 'Test');
}
},
watch: {
'grid.rows': {
deep: true,
handler: function (rows, oldRows) {
console.log('Rows updated', rows, oldRows);
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.js"></script>
<div id="app">
<table>
<tr v-for="row in grid.rows">
<td><input type="text" v-model="row.cell"></td>
<td><button type="button" #click="setRow(row)">Set</button></td>
</tr>
</table>
<button type="button" #click="addRow">Add</button>
</div>
If a row has not been edited manually, clicking "Set" button sets the field to "Test" and all the further updates to it are catched in watcher.
If a row has been edited manually first, watcher is not triggered, and clicking "Set" button does not immediately update the field. Adding another row updates the current row.
Is there a different way to add new array members? This page says it's ok to just add: https://v2.vuejs.org/v2/guide/list.html
I've figured out the issue... All the properties of objects should be initialized.
So in my case, the proper way to add a row is: this.grid.rows.push({cell: ''});
It would be nice though to initialize object properties when binding to controls, if not initialized yet.

Vue custom filtering input component

I'am trying to create a component that have 'just' an text input. String typed in this input will be used to filter a list. My problem is that I cannot handle how to share this filter string between my component and the main app that contains the list to filter.
I tried several things and most of the time I get the error :
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value
So I looked Vuex but I thinks it cannot help in this case because I can have several filter component used in he same page for different list, and I don't want them to be synchronized ^^
Here is what I have:
The filter component
<script type="x/template" id="filterTpl">
<div>
<span class="filter-wrapper">
<input type="search" class="input input-filter" v-model.trim="filter" />
</span>
</div>
</script>
<script>
Vue.component('list-filter', {
props: {
filter: String
}
template: '#filterTpl'
});
</script>
And my main app:
<div id="contacts">
<list-filter :filter="filter"></list-filter>
<ul class="contacts-list managed-list flex">
<li class="contact" v-for="contactGroup in filteredData">
[...]
</li>
</ul>
</div>
<script>
var contactsV = new Vue({
el: '#contacts',
data: {
filter: "",
studyContactsGroups: []
},
computed: {
filteredData: function(){
// Using this.filter to filter the studyContactsGroups data
[...]
return filteredContacts;
}
}
});
</script>
Thanks for any help or tips :)
You can synchronize child value and parent prop either via explicit prop-event connection or more concise v-bind with sync modifier:
new Vue({
el: '#app',
data: {
rawData: ['John', 'Jane', 'Jim', 'Eddy', 'Maggy', 'Trump', 'Che'],
filter: ''
},
components: {
'my-input' : {
// bind prop 'query' to value and
// #input update parent prop 'filter' via event used with '.sync'
template: `<input :value="query" #input="updateFilter">`,
props: ['query'],
methods: {
updateFilter: function(e) {
this.$emit('update:query', e.target.value) // this is described in documentation
}
}
}
},
computed: {
filteredData: function() {
// simple filter function
return this.rawData.filter(el => el.toLowerCase()
.match(this.filter.toLowerCase()))
}
}
});
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="app">
<my-input :query.sync="filter"></my-input>
<hr>
<ul>
<li v-for="line in filteredData">{{ line }}</li>
</ul>
</div>