Vue 2 With Jquery Chosen - vuejs2

Trying to use jquery-chosen with vue, the problem is that this plugin hides the actual select that I applied v-model, so when I select a value vue doesn't recognize it as a select change event and model value is not updated.
I've seen some solution available for Vue 1 that don't work with Vue 2
It's showing the current value but doesn't know how to set so that model value changes.
http://jsfiddle.net/q21ygz3h/
Vue.directive('chosen', {
twoWay: true, // note the two-way binding
bind: function(el, binding, vnode) {
Vue.nextTick(function() {
$(el).chosen().on('change', function(e, params) {
alert(el.value);
}.bind(binding));
});
},
update: function(el) {
// note that we have to notify chosen about update
// $(el).trigger("chosen:updated");
}
});
var vm = new Vue({
data: {
cities: ''
}
}).$mount("#search-results");

The preferred method of integrating jQuery plugins into Vue 2 is to wrap them in a component. Here is an example of your Chosen plugin wrapped in a component that handles both single and multiple selects.
Vue.component("chosen-select",{
props:{
value: [String, Array],
multiple: Boolean
},
template:`<select :multiple="multiple"><slot></slot></select>`,
mounted(){
$(this.$el)
.val(this.value)
.chosen()
.on("change", e => this.$emit('input', $(this.$el).val()))
},
watch:{
value(val){
$(this.$el).val(val).trigger('chosen:updated');
}
},
destroyed() {
$(this.$el).chosen('destroy');
}
})
And this is an example of usage in a template:
<chosen-select v-model='cities' multiple>
<option value="Toronto">Toronto</option>
<option value="Orleans">Orleans</option>
<option value="Denver">Denver</option>
</chosen-select>
<chosen-select v-model='cities2'>
<option value="Toronto">Toronto</option>
<option value="Orleans">Orleans</option>
<option value="Denver">Denver</option>
</chosen-select>
Fiddle for multiple select.
Original Answer
This component doesn't handle multiple selects correctly, but leaving it here because it was the original answer that was accepted.
Vue.component("chosen-select",{
props:["value"],
template:`<select class="cs-select" :value="value"><slot></slot></select>`,
mounted(){
$(this.$el)
.chosen()
.on("change", () => this.$emit('input', $(this.$el).val()))
}
})
This component supports v-model. So that you can use it in your template like so:
<chosen-select v-model='cities'>
<option value="Toronto">Toronto</option>
<option value="Orleans">Orleans</option>
</chosen-select>
Here is your fiddle updated.

For Vue3
<template>
<select><slot></slot></select>
</template>
<script>
export default {
props:{
value: [String, Array],
},
mounted() {
$(this.$el)
.val(this.value)
.chosen({disable_search: true})
.on("change", e => this.$emit('input', $(this.$el).val()))
},
watch: {
value(val){
$(this.$el).val(val).trigger('chosen:updated');
}
},
destroyed() {
$(this.$el).chosen('destroy');
},
}
</script>

Related

Clearing Vue JS v-for Select Field

I have a simple application that uses a v-for in a select statement that generates two select tags. The groupedSKUAttributes variable that creates the select statement looks like this:
groupedSKUAttributes = {colour: [{id: 1, name: 'colour', value: 'red'},
{id: 2, name: 'colour', value: 'blue'}],
size: [{id: 3, name: 'size', value: '40'},
{id: 4, name: 'size', value: '42'}]}
I also have a button that I want to clear the select fields. How do I get the clear method to make each of the select fields choose their default <option value='null' selected>select a {{ attributeName }}</option> value? I can't figure out if I'm meant to use a v-model here for the groupedSKUAttributes. Any advice would be appreciated.
The template looks like this:
<template>
<div>
<select
v-for='(attribute, attributeName) in groupedSKUAttributes'
:key='attribute'
#change='update(attributeName, $event.target.value)'>
<option value='null' selected>select a {{ attributeName }}</option>
<option
v-for='a in attribute'
:value='a.id'
:label='a.value'
:key='a.id'>
</option>
</select>
</div>
<button #click='clear'>clear</button>
</template>
And the JS script looks like this:
<script>
export default {
name: 'app',
data() {
return {
groupedSKUAttributes: null,
}
},
methods: {
clear() {
console.log('clear');
},
update(attributeName, attributeValue) {
console.log(attributeName, attributeValue);
},
getSKUAttributes() {
API
.get('/sku_attribute/get')
.then((res) => {
this.skuAttributes = res.data;
this.groupedSKUAttributes = this.groupBy(this.skuAttributes, 'name');
})
.catch((error) => {
console.error(error);
});
},
},
created() {
this.getSKUAttributes();
}
}
</script>
The v-model directive works within the v-for without any issues.
<script>
export default {
name: 'app',
data() {
return {
groupedSKUAttributes: null,
selected: {}
}
},
methods: {
clear() {
this.generateDefaultSelected(this.generateDefaultSelected);
},
update(attributeName, attributeValue) {
this.selected[attributeName] = attributeValue;
},
getSKUAttributes() {
API
.get('/sku_attribute/get')
.then((res) => {
this.skuAttributes = res.data;
this.groupedSKUAttributes = this.groupBy(this.skuAttributes, 'name');
// Call this method to reset v-model
this.generateDefaultSelected(this.groupedSKUAttributes);
})
.catch((error) => {
console.error(error);
});
},
generateDefaultSelected(groupedSKUAttributes) {
// Reset the object that maintains the v-model reference;
this.selected = {};
Object.keys(groupedSKUAttributes).forEach((name) => {
// Or, set it to the default value, you need to select
this.selected[name] = '';
});
}
},
created() {
this.getSKUAttributes();
}
}
</script>
In the above code, generateDefaultSelected method resets the selected object that maintains the v-model for all your selects.
In the template, you can use v-model or unidirectional value/#change pair:
<!-- Using v-model -->
<select
v-for='(attribute, attributeName) in groupedSKUAttributes'
:key='attributeName' v-model="selected[attributeName]">
<!-- Unidirection flow without v-model -->
<select
v-for='(attribute, attributeName) in groupedSKUAttributes'
:key='attributeName' :value="selected[attributeName]"
#change='update(attributeName, $event.target.value)'>

Change value without trigger watch again

I'm trying to find a workaround to the problem of changing 2-way binding inside a watch of the same property avoiding calling the watch again. for example:
<select v-model="language">
<option ... />
</select>
watch:{
language(newVal // 'fr' , oldVal // 'en'){
if(condition){
// do something
} else {
// roll back to the old language
this.language = "en" // will call watch again.
// Looking for something like this:
// Vue.set(this, 'language', 'en', { watch: false })
}
}
}
I thought about using #change but it won't help cause I have to set the value again with an object and not a plain value.
I know I can use other 2-way property and use it as a flag, but I look for something more elegant.
Why roll back the user's selection in the first place? You can use a computed property to provide a filtered list of valid options to provide a better user experience.
The example below will only let you select en if the condition checkbox is true.
Vue.config.productionTip = false;
new Vue({
el: '#app',
template: `
<div>
<p>Condition: <input type="checkbox" v-model="condition" /></p>
<p>Selection: {{selection}}</p>
<select v-model="selection">
<option v-for="opt in filteredOptions" :key="opt.value" :value="opt.value">{{opt.label}}</option>
</select>
</div>
`,
data: () => ({
selection: undefined,
condition: true,
options: [
{ label: 'English', value: 'en' },
{ label: 'French', value: 'fr' },
{ label: 'Spanish', value: 'es' },
],
}),
computed: {
filteredOptions() {
return this.condition ? this.options.filter(x => x.value === 'en') : this.options;
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>

how to make el-select and v-model work together when extracting a custom component

I'm using el-select to build a select component. Something like this:
<template>
//omitted code
<el-select v-model="filterForm.client"
filterable
remote
placeholder="Please enter a keyword"
:remote-method="filterClients"
:loading="loading">
<el-option
v-for="item in clientCandidates"
:key="item._id"
:label="item.name"
:value="item._id">
</el-option>
</el-select>
</template>
<scripts>
export default {
data() {
filterForm: {
client: ''
},
clientCandidates: [],
loading: false
},
methods: {
filterClients(query) {
if (query !== '') {
this.loading = true;
setTimeout(() => {
this.loading = false;
this.clientCandidates = [{_id: '1', name: 'foo'}, {_id: '2', name: 'bar'}];
}, 200);
} else {
this.clientCandidates = [];
}
}
}
}
</scripts>
So far so good, but since the component will appear in different pages, so I want to extract a custom component to avoid duplication.
According to the guideline,
v-model="fullName"
is equivalent to
v-bind:value="fullName"
v-on:input="$emit('input', $event)"
So I extracted the select component like this:
<template>
<el-select
v-bind:value="clientId"
v-on:input="$emit('input', $event)"
placeholder="Filter by short name"
filterable="true"
remote="true"
:remote-method="filter"
:loading="loading">
<el-option
v-for="item in clients"
:key="item._id"
:label="item.name"
:value="item._id">
</el-option>
</el-select>
</template>
<scripts>
export default {
props: {
clientId: {
type: String,
required: true
}
},
data() {
return {
clients: [],
loading: false,
}
},
methods: {
filter(query) {
if (query !== '') {
this.loading = true;
setTimeout(() => {
this.loading = false;
this.clients = [{_id: '1', name: 'foo'}, {_id: '2', name: 'bar'}];
}, 200);
} else {
this.clients = [];
}
}
}
}
</scripts>
And the parent component looks like this:
<select-client v-model="filterForm.clientId"></select-client>
The select drop down works fine, but unfortunately, the select does not reveal the option I selected, it remains empty after I choose an option. I suspect that maybe I should switch the v-on:input to 'v-on:change', but it does not work either.
UPDATE
I created a simple example, you can clone it here, please checkout the el-select-as-component branch. Run
npm install
npm run dev
You will see a simple page with 3 kinds of select:
The left one is a custom component written in raw select, it works fine.
The middle one is a custom component written in el-select, the dropdown remains empty but you can see the filterForm.elClientId in the console once you click Filter button. This is why I raise this question.
The right one is a plain el-select, it works fine.
The guideline says v-model is equivalent to v-bind:value and v-on:input but if you look closer, in the listener function, the variable binded is set with the event property. What you do in your exemple isn't the same, in your listener you emit another event. Unless you catch this new event, your value will never be set.
Another thing is you can't modify a props, you should consider it like a read-only variable.
If you want to listen from the parent to the emitted event into the child component, you have to do something like this
<template>
<el-select
:value="selected"
#input="dispatch"
placeholder="Filter by short name"
:filterable="true"
:remote="true"
:remote-method="filter"
:loading="loading">
<el-option
v-for="item in clients"
:key="item._id"
:label="item.name"
:value="item._id">
</el-option>
</el-select>
</template>
<script>
export default {
name: 'SelectClient',
data() {
return {
selected: '',
clients: [],
loading: false,
}
},
methods: {
filter(query) {
if (query !== '') {
this.loading = true;
setTimeout(() => {
this.loading = false
this.clients = [{_id: '1', name: 'foo'}, {_id: '2', name: 'bar'}]
}, 200)
} else {
this.clients = []
}
},
dispatch (e) {
this.$emit('input', e)
this.selected = e
}
}
}
</script>
NB: a v-model + watch pattern will work too. The important thing is to $emit the input event, so the v-model in the parent will be updated.
And in your parent you can use this component like this: <select-client v-model="clientId"/>.
Tips: if you want to modify the same data in different place, you should have a single source of truth and prefer something like vuex. Then your component will be like this
<template lang="html">
<select
v-model="clientId">
<option
disabled
value="">Please select one</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
</template>
<script>
export default {
data () {
return {
clientId: ''
}
},
watch: {
clientId (newValue) {
// Do something else here if you want then commit it
// Of course, listen for the 'setClientId' mutation in your store
this.$store.commit('setClientId', newValue)
}
}
}
</script>
Then in your other components, you can listen to $store.state.clientId value.

How to pass changed value to other component's methods in Vue.js?

In the header component, let's call it App.vue, there is a select element:
<select v-model="locale">
<option value="en">English</option>
<option value="pl">Polski</option>
</select>
In the same component, the option selected by user gets processed in watch:
watch: {
locale (val) {
this.$i18n.locale = val;
console.log("locale: ", val);
localStorage.setItem("userPrefLang", val);
}
},
How can I notify other components (siblings, not children), let's say Users.vue, that the locale parameter was changed? I'd like to pick up the new value in the code (using a JS method), not with bound DOM elements. The new value should trigger the page reload due to changed locales. Should I use Users.vue's watcher, props, or is there any other way?
One idea is to have a root component to manage the communication between the two peers, making your page look like this:
<div id="root">
<app></app>
<users></users>
<div>
With this beginning, a next step could be passing the locale to users via props:
<div id="root">
<app></app>
<users :locale="rootLocale"></users>
<div>
To update rootLocale, we could listen for an event emitted by app:
<div id="root">
<app #locale-changed="localeChanged"></app>
<users :locale="rootLocale"></users>
<div>
Putting it together, here are the component definitions:
var app = {
name: 'app',
template:
`<select v-model="locale">
<option value="en">English</option>
<option value="pl">Polski</option>
</select>`,
data: function () {
return {
locale: "en"
}
},
watch: {
locale: function () {
this.$emit('locale-changed', this.locale);
}
},
};
var users = {
name: 'users',
template:
`<div>
<div>{{message}}</div>
<div>{{locale}}</div>
</div>`,
props: ['locale'],
data: function () {
return {
message: 'awaiting change'
}
},
watch: {
locale: function () {
this.message = 'locale changed'
}
}
};
And here is the root element:
var vm = new Vue({
el: "#root",
components: { app, users },
data: function () {
return {
rootLocale: ''
}
},
methods: {
localeChanged: function (val) {
this.rootLocale = val;
}
}
});
Full demo is on JsFidde: https://jsfiddle.net/zfp5rLb7/1/
Does that answer your question?
Vue has a way of doing this, event bus here is a link from the docs which tell you how you can do this
https://v2.vuejs.org/v2/guide/components.html#Non-Parent-Child-Communication
You can also refer this helpful article
https://alligator.io/vuejs/global-event-bus/
It can look something like this
var bus = new Vue()
// in component A's method
bus.$emit('language-change', 'en')
// in component B's created hook
bus.$on('language-change', function (language) {
// ...
})

vuejs2 and chosen select issue

Good day, pls have a look at this bin. It was written Vue 0.12 version and chosen js. How can i make it work with vue2 version. i really need this as a directive not as a component.
`<div id='search`-results'>
Vue model value <br>
{{city | json}}
<hr>
Select value:
<!-- note the `v-model` and argument for `v-chosen` -->
<select class="cs-select" v-model="city" options="cities" v-chosen="city"></select>
<select v-model="city" options="cities"></select>
Vue.directive('chosen', {
twoWay: true, // note the two-way binding
bind: function () {
$(this.el)
.chosen({
inherit_select_classes: true,
width: '30%',
disable_search_threshold: 999
})
.change(function(ev) {
this.set(this.el.value);
}.bind(this));
},
update: function(nv, ov) {
// note that we have to notify chosen about update
$(this.el).trigger("chosen:updated");
}
});
var vm = new Vue({
data: {
city: 'Toronto',
cities: [{text: 'Toronto', value: 'Toronto'},
{text: 'Orleans', value: 'Orleans'}]
}
}).$mount("#search-results");
Here it is implemented as a wrapper component that supports v-model and a slot for the options. This makes it a drop-in replacement for a standard select widget, at least as far as basic functionality. The updated(), happily, will notice changes to the options list as well as to the value.
Since two-way directives are not supported in Vue2, I do not believe there is a way to implement this as a directive. If you really need that, you will want to use Vue1.
var vm = new Vue({
el: '#search-results',
data: {
city: 'Toronto',
cities: [{
text: 'Toronto',
value: 'Toronto'
}, {
text: 'Orleans',
value: 'Orleans'
}]
},
components: {
'chosenSelect': {
template: '<select class="cs-select" v-model="proxyValue" ><slot></slot></select>',
props: ['value', 'options'],
computed: {
proxyValue: {
get() {
return this.value;
},
set(newValue) {
this.$emit('input', newValue);
}
}
},
mounted() {
$(this.$el)
.chosen({
inherit_select_classes: true,
width: '30%',
disable_search_threshold: 999
})
.change((ev) => {
this.proxyValue = ev.target.value;
});
},
updated() {
$(this.$el).trigger('chosen:updated');
}
}
}
});
setTimeout(() => { vm.cities.push({text: 'Houston', value: 'Worth it'}); }, 1000);
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/chosen/1.4.2/chosen.proto.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/chosen/1.4.2/chosen.jquery.min.js"></script>
<link href="//cdnjs.cloudflare.com/ajax/libs/chosen/1.4.2/chosen.css" rel="stylesheet" />
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.3/vue.min.js"></script>
<div id='search-results'>
Vue model value
<br> {{city | json}}
<hr> Select value:
<chosen-select v-model="city">
<option v-for="item in cities" :value="item.value">{{item.text}}</option>
</chosen-select>
<select v-model="city">
<option v-for="item in cities" :value="item.value">{{item.text}}</option>
</select>
</div>