How to differentiate between user input and data change with vue-select - javascript

I'm using vue-select in my app and am trying to prevent an event handler from firing when the default values are first loaded into the vue-select input.
The component looks like this:
<v-select
multiple
v-model="product.recommended_accessories"
label="name"
:options="accessoryOptions"
#input="saveProduct"
#search="onAccessorySearch">
<template slot="option" slot-scope="option">
<h4>{{ option.name }}</h4>
<h5>{{ option.sku }}</h5>
</template>
</v-select>
As you can see, I want to save the product when the user changes the values in this multi-select. It works fine, but with one issue.
The value of the select is tied to the product.recommended_accessories data. Elsewhere in the app, a product is loaded from the server, which includes a recommended_accessories attribute. Loading the product then triggers saveProduct to be called since vue-select set the preselected options for the input, which apparently triggers the #input event.
Is there anyway around this? Maybe I have made some sort of design error here. Or maybe there is a hook I should be using to bind the event handler, or to set some sort of flag indicating that the product is in the process of being loaded and saving the product shouldn't occur.
I'm just trying to avoid saving the product immediately after it's loaded for no reason.

For now I am just tracking an accessoryEventCount variable that gets initialized to 0 whenever a product is loaded. Then I make sure accessoryEventCount > 0 before calling saveProduct on an v-select input event.
It works, but I am still wondering if there is a more elegant solution.
UPDATE
Looks like Vue.nextTick is what I was looking for. Before setting the value of product in code, I set a flag this.isSettingProduct = true. Then I set the product, and call Vue.nextTick(() => { this.isSettingProduct = false });.
Now I can avoid saving the product if this.isSettingProduct == true. Using Vue.nextTick ensures that the flag isn't set back to false until after the asynchronous data update completes.

It looks you should bind prop=onChange, though #input still seems working (check v-select github: line# 544).
Below is my solution, before loading your product, bind onChange with function () {}, after loaded, bind it with the function you like.
Vue.component('v-select', VueSelect.VueSelect)
app = new Vue({
el: "#app",
data: {
accessoryOptions: [
{'name':'a1', 'sku':'a2'},
{'name':'b1', 'sku':'b2'},
{'name':'c1', 'sku':'c2'}
],
product: {
recommended_accessories: []
},
saveProduct: function (){}
},
methods: {
onAccessorySearch: function () {
console.log('emit input')
},
loadProduct: function () {
this.product.recommended_accessories = ['a1', 'b1'] // simulate get data from the server
setTimeout( () => {
this.saveProduct = (value) => {
console.log('emit input', this.product.recommended_accessories)
}
}, 400)
}
}
})
#app {
width: 400px;
}
<script src="https://unpkg.com/vue#latest"></script>
<script src="https://unpkg.com/vue-select#latest"></script>
<div id="app">
<button #click="loadProduct()">Click Me!</button>
<v-select
multiple
v-model="product.recommended_accessories"
label="name"
:options="accessoryOptions"
:on-change="saveProduct"
#search="onAccessorySearch">
<template slot="option" slot-scope="option">
<span>{{ option.name }}</span>
<span>{{ option.sku }}</span>
</template>
</v-select>
</div>

Related

console.log() does not show actual value

I am iterating over a list on which I used a #click function to update some other elements. The elements indexes values are binded to selectedFixtures. I don't understand why, when I click on an element, it prints out [] and the second time I click on an element, it prints out the old value ex.: [1]. How can I work with the actual values in my function so it prints out [1] on the first time?
<v-chip-group
v-model="selectedFixtures"
multiple
>
<v-chip v-for="(f, i) in filteredFixtures()" :key="i" #click="selectFixture(f, i)">
<div class="d-flex align-baseline">
<span class="font-weight-medium">{{ f.name }}</span>
</div>
</v-chip>
</v-chip-group>
methods: {
selectFixture(f, index) {
console.log(JSON.stringify(this.selectedFixtures));
if (this.selectedFixtures.length === 1) {
console.log('here!')
}
}
}
You are wanting an array of chips chosen, so you are missing multiple from your template (is this just a typo when writing question?). As to your issue with old value, when the click event happens, selectedFixtures has yet to be updated with the value added/removed. You need to wait a tick to get the correct result:
Added multiple to template:
<v-chip-group v-model="selectedFixtures" multiple>
and adding waiting a tick in the function:
this.$nextTick(() => {
if (this.selectedFixtures.length === 1) {
console.log('here')
}
})
CODEPEN
PS, as a recommendation, don't call functions in template when not needing to, I'm talking about filteredFixtures(), you could instead use a computed property.

Data is not updated when using as object, but changes normally when it's a variable

I'm writing a function to update a custom checkbox when clicked (and I don't want to use native checkbox for some reasons).
The code for checkbox is
<div class="tick-box" :class="{ tick: isTicked }" #click="() => isTicked = !isTicked"></div>
which works find.
However, there are so many checkboxes, so I use object to keep track for each item. It looks like this
<!-- (inside v-for) -->
<div class="tick-box" :class="{ tick: isTicked['lyr'+layer.lyr_id] }" #click="() => {
isTicked['lyr'+layer.lyr_id] = !isTicked['lyr'+layer.lyr_id]
}"></div>
Now nothing happens, no error at all.
When I want to see isTicked value with {{ isTicked }}, it's just shows {}.
This is what I define in the <script></script> part.
export default {
data() {
return {
isTicked: {},
...
};
},
...
}
Could you help me where I get it wrong?
Thanks!
Edit:
I know that declaring as isTicked: {}, the first few clicks won't do anything because its proerty is undefined. However, it should be defined by the first/second click not something like this.
Objects does not reflect the changes when updated like this.
You should use $set to set object properties in order to make them reactive.
Try as below
<div class="tick-box" :class="{ tick: isTicked['lyr'+layer.lyr_id] }" #click="onChecked"></div>
Add below method:
onChecked() {
this.$set(this.isTicked,'lyr'+this.layer.lyr_id, !this.isTicked['lyr'+this.layer.lyr_id])
}
VueJS watches data by reference so to update object in state you need create new one.
onChecked(lyr_id) {
const key = 'lyr'+lyr_id;
this.isTicked = {...this.isTicked, [key]: !this.isTicked[key]};
}

vuetify v-select with multiple options - getting selected/deselected option

I'm using Vuetify and its v-select component with multiple option enabled to allow selecting multiple options.
These options represent talent(candidate) pools for my CRM software.
What it needs to do is that when some option in v-select is checked, candidates from checked talent pool are fetched from API and saved to some array (let's call it markedCandidates), and when option is deselected, candidates from that pool need to be removed from markedCandidates array.
The problem is that #change or #input events return complete list of selected options. I need it to return just selected/deselected pool and information if it's selected or deselected, to be able to update the markedCandidates array.
My existing code:
<v-select return-object multiple #change="loadCandidatesFromTalentPool" v-model="markedCandidates" :item-text="'name'" :item-value="'name'" :items="talentPoolsSortedByName" dense placeholder="No pool selected" label="Talent Pools" color='#009FFF'>
<template slot="selection" slot-scope="{ item, index }">
<span v-if="index === 0">{{ item.name }}</span>
<span v-if="index === 1" class="grey--text caption othersSpan">(+{{ talentPools.length - 1 }} others)</span>
</template>
</v-select>
Any idea how to solve this?
As I said, loadCandidatesFromTalentPool(change) returns complete array of v-model (markedCandidates)..
EDIT:
I found this solution, it's more of a workaround actually, would be nice if there was dedicated event for this situation:
https://codepen.io/johnjleider/pen/OByoOq?editors=1011
Actually there is only one event related to changing values of v-autocomplete : #change (See https://vuetifyjs.com/en/components/autocompletes#events). The watch approach is useful if you want to monitor only individual changes. However, if you plan to do this with more selectors, it could be better if you create a custom reusable component with a new attached event (in this example, for the last change).
Vue.component('customselector',{
props:[
"value",
"items"
],
data: function() {
return {
content: this.value,
oldVal : this.value
}
},
methods: {
handleInput (e) {
this.$emit('input', this.content)
},
changed (val) {
oldVal=this.oldVal
//detect differences
const diff = [
...val.filter(x => !oldVal.includes(x)),
...oldVal.filter(x => !val.includes(x))
]
this.oldVal = val
var deleted=[]
var inserted=[]
// detect inserted/deleted
for(var i=0;i<diff.length;i++){
if (val.indexOf(diff[i])){
deleted.push(diff[i])
}else{
inserted.push(diff[i])
}
}
this.$emit("change",val)
this.$emit("lastchange",diff,inserted,deleted);
}
},
extends: 'v-autocomplete',
template: '<v-autocomplete #input="handleInput" #change="changed" :items="items" box chips color="blue lighten-2" label="Select" item-text="name" item-value="name" multiple return-object><slot></slot></v-autocomplete>',
})
Then you can use your component as simple as:
<customselector #lastchange="lastChange" >...</customselector>
methods:{
lastChange: function(changed, inserted, deleted){
this.lastChanged = changed
}
}
The changed only shows items which are actually changed. I've added the inserted and deleted arrays to return new items added or removed from the selection.
Source example: https://codepen.io/fraigo/pen/qQRvve/?editors=1011
Replace
:item-text="'name'" :item-value="'name'"
by
item-text="name" item-value="name"

Getting the event source from custom events in Vue?

I'm building a Vue component that consists of an unspecified number of child components. Only one child component is visible at all times, and the user can only switch between child components when the one currently visible has emitted an is-valid event.
I want to keep this decoupled, such that children do not know about their parent and only communicate by emitting events. This also means that the children do not know their position within the parent component.
So, the parent component somehow has to keep track of which child the event came from. If the event came from the right child (the one currently visible) then some buttons are activated that allows the user to go to the next or previous child.
Here's my code so far:
HTML
<div id="app">
<template id="m-child">
<div>
<button v-on:click="setstate(true)">Valid</button>
<button v-on:click="setstate(false)">Invalid</button>
</div>
</template>
<template id="m-parent">
<div>
<m-child v-on:newstate="newchildstate"></m-child>
<m-child v-on:newstate="newchildstate"></m-child>
<m-child v-on:newstate="newchildstate"></m-child>
</div>
</template>
<m-parent></m-parent>
</div>
JS
Vue.component('m-child', {
template: '#m-child',
data: function() {
return {};
},
methods: {
setstate: function (valid) {
this.$emit('newstate', valid);
}
}
});
Vue.component('m-parent', {
template: '#m-parent',
methods: {
newchildstate: function (valid) {
console.log('valid:' + valid + ', but where from?');
}
}
});
new Vue({
el: '#app'
});
Of course I could hardcode an index on the child event binding:
<m-child v-on:newstate="newchildstate(0, $event)"></m-child>
<m-child v-on:newstate="newchildstate(1, $event)"></m-child>
<m-child v-on:newstate="newchildstate(2, $event)"></m-child>
But that would make the whole setup a lot less modular, I just want to be able to plug in a number of children in the DOM and make it work right away.
I've looked at the API for Vue events and there doesn't seem to be a way to get the source from the event object.
This depends on what you want to receive back, my personal preference is to pass in a prop to set a unique id and pass it back in the $emit:
<m-child v-on:newstate="newchildstate" :id="1"></m-child>
<m-child v-on:newstate="newchildstate" :id="2"></m-child>
<m-child v-on:newstate="newchildstate" :id="3"></m-child>
Then in your child component you can emit an object with the state and id the id:
Child:
this.$emit('newstate', {id: this.id, state: valid});
Parent:
newchildstate: function (valid) {
console.log('valid:' + valid.state + ', from' + valid.id);
}
I realise that this doesn't look hugely different from your hard coded example, but at some point your parent is going to want to deal with the event, so you could set up an array in data with the initial states and then use a v-for:
data: {
children: [true, false, false] // setup states
}
You would then do:
<div v-for="(state, index) in states">
<m-child v-on:newstate="newchildstate" :id="index"></m-child>
</div>
And in your view model:
methods: {
newchildstate: function(valid) {
this.$set(this.states, valid.id, valid.state);
}
}
Here's a JSFiddle that initiates the array dynamically via a prop and sets up the child components: https://jsfiddle.net/2y9727e2/

How to fire function after v-model change?

I have input which I use to filter my array of objects in Vue. I'm using Salvattore to build a grid of my filtered elements, but it doesn't work too well. I think I have to call rescanMediaQueries(); function after my v-model changes but can't figure how.
Here is my Vue instance:
var articlesVM = new Vue({
el: '#search',
data: {
articles: [],
searchInput: null
},
ready: function() {
this.$http.get('posts').then(function (response) {
this.articles = response.body;
});
}
});
And here is how I have built my search
<div class="container" id="search">
<div class="input-field col s6 m4">
<input v-model="searchInput" class="center-align" id="searchInput" type="text" >
<label class="center-align" for="searchInput"> search... </label>
</div>
<div id="search-grid" v-show="searchInput" data-columns>
<article v-for="article in articles | filterBy searchInput">
<div class="card">
<div class="card-image" v-if="article.media" v-html="article.media"></div>
<div class="card-content">
<h2 class="card-title center-align">
<a v-bind:href="article.link">{{ article.title }}</a>
</h2>
<div class="card-excerpt" v-html="article.excerpt"></div>
</div>
<div class="card-action">
<a v-bind:href="article.link"><?php _e('Read More', 'sage'); ?></a>
</div>
</div>
</article>
</div>
I did get the grid system working by adding watch option to my Vue, but every time I wrote something to my input and then erase it my filterBy method wouldn't work at all. It didn't populate any data even if I tried to retype the same keyword as earlier. Here is the watch option I used:
watch: {
searchInput: function (){
salvattore.rescanMediaQueries();
}
}
I think your problem is with the scoping of this in your success handler for http. Your articles object in Vue component is not getting any values from your http.get(..) success handler.
Inside your ready function, your http success handler should be as follows:
this.$http.get('posts').then(response => {
this.articles = response.body; // 'this' belongs to outside scope
});`
Alternatively you can also do:
var self = this; // self points to 'this' of Vue component
this.$http.get('posts').then(response => {
self.articles = response.body; // 'self' points to 'this' of outside scope
});`
Another similar issue: https://stackoverflow.com/a/40090728/654825
One more thing - it is preferable to define data as a function, as follows:
var articlesVM = new Vue({
el: '#search',
data: function() {
return {
articles: [],
searchInput: null
}
},
...
}
This ensures that your articles object is unique to this instance of the component (when you use the same component at multiple places within your app).
Edited after comment #1
The following code seems to work alright, the watch function works flawlessly:
var vm = new Vue({
el: '#search',
template: `<input v-model="searchInput" class="center-align" id="searchInput" type="text" >`,
data: {
searchInput: ""
},
watch: {
searchInput: function() {
console.log("searchInput changed to " + this.searchInput);
}
}
})
The input in template is an exact copy of your version - I have even set the id along with v-model, though I do not see the reason to set an id
Vue.js version: 2.0.3
I am unable to see any further, based on details in the question. Can you check if your code matches with the one above and see if you can get the console debugging messages?
Edited after comment #4, #5
Here is another thought which you need to verify:
Role of vue.js: Render the DOM
Role of salvattore plugin: Make the DOM layouts using CSS only
Assuming the above is true for salvattore plugin, and hopefully it does not mess with vue.js observers / getters / setters, then you can do the following: provide a time delay of about 50 ms so that vue.js completes the rendering, and then call the salvattore plugin to perform the layouts.
So your watch function needs to be as follows:
watch: {
searchInput: function (){
setTimeout(function(){
salvattore.rescanMediaQueries();
}, 50);
}
}
Alternatively you may also use Vue.nexttick() as follows:
Vue.nextTick(function () {
// DOM updated
})
The nextTick is documented here: https://vuejs.org/api/#Vue-nextTick
I do not know if you may need to provide a little bit of extra time for salvattore plugin to start the layouts, but one of the above should work out.
Let me know if it works!

Categories