Vuejs directive binding a model - javascript

I'm using a Vuejs 1.0 directive to turn a select field (single and multiple) into a Select2 jQuery plugin field.
Vue.directive('select2', {
twoWay: true,
priority: 1000,
params: ['max-items'],
bind: function () {
var self = this;
console.log(self.params);
$(this.el)
.select2({
maximumSelectionLength: self.params.maxItems,
theme: 'bootstrap',
closeOnSelect: true
})
.on('change', function () {
var i, len, option, ref, values;
if (self.el.hasAttribute('multiple')) {
values = [];
ref = self.el.selectedOptions;
for (i = 0, len = ref.length; i < len; i++) {
option = ref[i];
values.push(option.value);
}
return self.set(values);
} else {
return self.set(self.el.value);
}
})
},
update: function (value, oldValue) {
$(this.el).val(value).trigger('change')
},
unbind: function () {
$(this.el).off().select2('destroy')
}
});
This all works fine. I'm also trying to bind a model to the value of the field but can't seem to get it to bind properly.
<select class="form-control" name="genre" v-model="upload.genre" v-select2="">
<option value="50">Abstract</option>
<option value="159">Acapella</option>
<option value="80">Acid</option>
...
</select>
The upload.genre property does not update automatically.

v-model is actually syntactic sugar on passing a prop and on change event setting the value, so following:
<input v-model="something">
is equivalent of
<input v-bind:value="something" v-on:input="something = $event.target.value">
You have to also make similar changes, You can see this type code in the select2 example provided by vue team.
.on('change', function () {
vm.$emit('input', this.value)
})
With Vue 1.0
As you are using vue 1.0, there is a two-way options for directives which helps to write data back to the Vue instance, you need to pass in twoWay: true. This option allows the use of this.set(value) inside the directive:
Vue.directive('select2', {
twoWay: true,
bind: function () {
this.handler = function () {
// set data back to the vm.
// If the directive is bound as v-select2="upload.genre",
// this will attempt to set `vm.upload.genre` with the
// given value.
this.set(this.el.value)
}.bind(this)
this.el.addEventListener('input', this.handler)
},
unbind: function () {
this.el.removeEventListener('input', this.handler)
}
})
and in HTML:
<select class="form-control" name="genre" v-select2="upload.genre">

Related

watch hook multiple variable in Vuejs [duplicate]

My data object:
data: {
selected: {
'type': null,
'instrument': null
},
My template:
<select v-model="selected['instrument']" #change="switchFilter('instrument', $event)">
<option v-for="instrument in instruments" :value="instrument.value">#{{ instrument.text }}</option>
</select>
<select v-model="selected['type']" #change="switchFilter('type', $event)">
<option v-for="type in types" :value="type.value">#{{ type.text }}</option>
</select>
How can I watch both selected indexes at the same time? I want to do something like this everytime any of the indexes updates:
watch: {
selected: function(o, n) {
...
}
}
You can use deep option provided by the watcher from vue. As stated in the docs:
To also detect nested value changes inside Objects, you need to pass in deep: true in the options argument. Note that you don’t need to do so to listen for Array mutations.
You code will look like following:
watch: {
'selected': {
handler: function (val, oldVal) {
console.log('watch 1', 'newval: ', val, ' oldVal:', oldVal)
},
deep: true
}
}
I think you can do this:
watch: {
$data: {
handler: function(val, oldVal) {
console.log(val)
},
deep: true
}
},
watch: {
'selected.type': function (newSelectedType) {
console.log(newSelectedType)
},
'selected.instrument': function (newSelectedinstrument) {
console.log(newSelectedinstrument)
}
}
If you are just trying to calculate a new data from selected, you can just use computed properties, since the data of Vue are reactive, the computed properties can also detect the changes of data.
If you want to use a single function to watch the entire object, you can use $watch with deep: true:
mounted () {
this.$watch('$data.selected', this.onSelectedUpdate, { deep: true })
}
note that '$data.selected' is a string, Vue will parse it.
and in your methods:
onSelectedUpdate (newSelected) {
console.log(newSelected)
}

Vue Directive not updating it's value - Jquery Chosen

I am using Jquery Chosen along with Vue. This is my Vue directive:
Vue.component("chosen-select", {
props: {
value: [String, Array],
multiple: Boolean
},
template: `<select :multiple="multiple"><slot></slot></select>`,
mounted() {
$(this.$el)
.val(this.value)
.chosen({ width: '100%' })
.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 using it like this:
<chosen-select v-model="basicDetailsModel.stateID" v-validate="'required'" data-vv-as="state" :state="errors.has('stateID') ? 'invalid' : 'valid'" name="stateID">
<option :value="null">Please select an option</option>
<option v-for="(state, index) in states" :key="index" :value="state.sid">{{state.nm}}</option>
</chosen-select>
If the states are assigned static value it works fine as per expectation but if I fetch the states value dynamically the chosen is not updated with latest values. It stays with the initial values.
How would I fix this issue?
Edit: This one works. Do you think this is the right way?
Vue.component("chosen-select", {
data() {
return { observer: null }
},
props: {
value: [String, Array],
multiple: Boolean
},
template: `<select :multiple="multiple"><slot></slot></select>`,
mounted() {
// Create the observer (and what to do on changes...)
this.observer = new MutationObserver(function (mutations) {
$(this.$el).trigger("chosen:updated");
}.bind(this));
// Setup the observer
this.observer.observe(
$(this.$el)[0],
{ childList: true }
);
$(this.$el)
.val(this.value)
.chosen({ width: '100%' })
.on("change", e => this.$emit('input', $(this.$el).val()))
},
watch: {
value(val) {
$(this.$el).val(val);
}
},
destroyed() {
$(this.$el).chosen('destroy');
}
});
The easiest way to fix this issue is simply not to render the select until you have options to render using v-if.
<chosen-select v-if="states && states.length > 0" v-model="basicDetailsModel.stateID" v-validate="'required'" data-vv-as="state" :state="errors.has('stateID') ? 'invalid' : 'valid'" name="stateID">
You could also play around with emitting the chosen:updated event when the component is updated.
updated(){
$(this.$el).trigger("chosen:updated")
},
which works for multiple selects, but mysteriously not for single selects.
I am not sure how you are fetching the states dynamically, but if you're using jQuery to get them, then I think that is your problem. Vue doesn't get notified if non-Vue things (like jQuery) change anything.
Even if that's not the case, this is worth reading to see why jQuery and Vue don't get along.
Can you add how you are fetching them dynamically?
Also, consider using a Vue framework like Vuetify which has a pretty good select control and is totally in Vue.

Vue.js: Passing anonymous functions in props object

I'm trying to extend the select2 example to be more practically useful. So far I've added multiselect functionality, and trying to allow custom select2 configuration.
https://jsfiddle.net/5ytm3LL6/
It appears that function properties of props objects are being stripped when handed to component.
What is a solution for a parent to give widget component configuration with js functions?
I'm somewhat confused, because the embedded version actually shows params being transferred properly in the built-in console output, while jsfiddle browser console output does not. However, both versions do not pass the functions to select2 widget.
Vue.component('select2', {
props: ['options', 'value', 'params'],
template: '<select><slot></slot></select>',
mounted: function () {
var vm = this, params = $.extend({}, this.params || {});
console.log(this.params, params);
params.data = this.options;
$(this.$el).val(this.value).select2(params).on('change', function () {
vm.$emit('input', $(vm.$el).val());
});
},
watch: {
value: function (value) {
var $el = $(this.$el);
if (!_.isEqual($el.val(), value)) {
$el.select2('val', value);
}
},
options: function (options) {
$(this.$el).select2({ data: options });
}
},
destroyed: function () { $(this.$el).off().select2('destroy'); }
});
new Vue({
el:'#app',
data: function () {
return {
value: 'a',
options: [{id:'a', label:'A'}, {id:'b', label:'B'}],
params: {
test: 'TEST',
formatSelection: function (item) {return item.label;},
formatResult: function (item) {return item.label;}
}
};
}
});
<link rel="stylesheet" type="text/css" href="https://unpkg.com/select2/dist/css/select2.css"></style>
<script src="https://unpkg.com/lodash"></script>
<script src="https://unpkg.com/jquery"></script>
<script src="https://unpkg.com/select2"></script>
<script src="https://unpkg.com/vue"></script>
<div id="app">
<select2 v-model="value" :options="options" :params="params"></select2>
</div>
I see js functions you have passed being printed in console log. I have updated your fiddle and put following logs in select2 compoent:
console.log(params.formatResult);
console.log(params.formatSelection);
Which prints following:
function (item) {return item.label;}
function (item) {return item.label;}

knockout subscription not working

I have been trying to subscribe to when a dropdown value changes. I have the following logic however I cannot seem to get it working.
HTML
<div id="case-pin-#modelItem.CaseID" data-caseid="#modelItem.CaseID" class="row hidden popovercontainer pinBinding">
<select data-bind="options:userPins,
value:selectedPin,
optionsCaption:'-- please select --',
optionsText: 'Name',
optionsValue: 'Id'"></select>
</div>
JS
function UserPinViewModel(caseId) {
var self = this;
self.selectedPin = ko.observable();
self.userPins = ko.observableArray([]);
self.caseId = caseId;
self.selectedPin.subscribe(function (newValue) {
console.log(newValue);
//addCaseToPin(newValue, self.caseId);
});
}
var pinObjs = [];
$(function () {
pinObjs = [];
$(".pinBinding").each(function () {
var caseId = this.getAttribute("data-caseid");
var view = new UserPinViewModel(caseId);
pinObjs.push(view);
ko.cleanNode(this);
ko.applyBindings(view, this);
});
})
The userPins array is populated by an AJAX call to the server as the values in the dropdown are dependent upon another section of the website which can change the values in the dropdown - here the logic I have used to populate the array.
function getPins() {
$.ajax({
type: 'POST',
url: '/Home/GetPins',
success: function (data) {
for (var i = 0; i < pinObjs.length; i++) {
pinObjs[i].userPins(data);
}
},
error: function (request, status, error) {
alert("Oooopppppsss! Something went wrong - " + error);
}
});
}
The actual values in the dropdowns all change to match what is returned from the server however whenever I manually change the dropdown, the subscription event is not fired.
You're using both jQuery and Knockout to manipulate the DOM, which is not a good idea. The whole idea of Knockout is that you don't manipulate the DOM, it does. You manipulate your viewModel.
Using cleanNode is also a code smell, indicating that you're doing things the wrong way. Knockout will handle that if you use the tools Knockout provides.
In this case, I was going to suggest a custom binding handler, but it looks like all you really want is to have a UserPinViewModel object created and applied to each instance of your .pinBinding element in the HTML. You can do that using the with binding, if you expose the UserPinViewModel constructor in your viewModel.
function UserPinViewModel(caseId) {
var self = this;
self.selectedPin = ko.observable();
self.userPins = ko.observableArray([]);
self.caseId = caseId;
self.selectedPin.subscribe(function(newValue) {
console.log(newValue);
//addCaseToPin(newValue, self.caseId);
});
// Pretend Ajax call to set pins
setTimeout(() => {
self.userPins([{
Name: 'option1',
Id: 1
}, {
Name: 'option2',
Id: 2
}, {
Name: 'option3',
Id: 3
}])
}, 800);
// Later, the options change
setTimeout(() => {
self.userPins([{
Name: 'animal1',
Id: 'Elephant'
}, {
Name: 'animal2',
Id: 'Pony'
}, {
Name: 'animal3',
Id: 'Donkey'
}])
}, 4000);
}
ko.bindingHandlers.pin = {
init: () => null,
update: () => null
};
ko.applyBindings({
pinVm: UserPinViewModel
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div id="case-pin-#modelItem.CaseID" data-bind="with: new pinVm('someCaseId')" class="row hidden popovercontainer pinBinding">
<select data-bind="options:userPins,
value:selectedPin,
optionsCaption:'-- please select --',
optionsText: 'Name',
optionsValue: 'Id'"></select>
</div>
Your getPins function suggests that the .pinBinding elements should correspond to the data being received. In that case, pinObjs should really be a part of your viewModel, and the elements should be generated (perhaps in a foreach) from the data, rather than being hard-coded. I don't know how that works with what I presume is the server-side #modelItem.CaseID, though.

Vue js apply filter on v-model in an input field

Hope someone can help me! I have made a directive wrapping the Jasny Bootstrap Plugin more specifically the input-mask thing and everything goes well!
Now I have made a custom filter supported by moment to format the date field!
The date format that I receive from my backend application is YYY-MM-DD and I must show on the view as DD/MM/YYYY... I've tried v-model="date | myDate" but it didn't work properly!
JS
Vue.directive('input-mask', {
params: ['mask'],
bind: function() {
$(this.el).inputmask({
mask: this.params.mask
});
},
});
Vue.filter('my-date', function(value, formatString) {
if (value != undefined)
return '';
if (formatString != undefined)
return moment(value).format(formatString);
return moment(value).format('DD/MM/YYYY');
});
var vm = new Vue({
el: 'body',
data: {
date: '2015-06-26',
}
});
HTML
<label>Date</label>
<input type="text" class="form-control" v-input-mask mask="99/99/9999" v-model="date">
<p>{{ date | myDate 'dd/mm/yyyy' }}</p>
There is the JSBin if somebody's interested!
Thanks in advance!
EDIT: Explaining better what I expect =)
When the page first load the input receive the value of 2015-06-26 and I would like to show that value as DD/MM/YYYY so 26/06/2015! It works properly only after I start typing something!
I understand what you are trying to do, however, because of the two way binding when using v-model, it may be better to just format the date as you receive it from the server, and then, use it with the desired format in your front-end app ('DD/MM/YYYY').
When sending the data back to the back-end, you just format it back to the desired server format ('YYYY-MM-DD').
In your Vue app, the work flow would be something like this:
new Vue({
el: 'body',
data: {
date: null,
},
methods: {
getDataFromServer: function() {
// Ajax call to get data from server
// Let's pretend the received date data was saved in a variable (serverDate)
// We will hardcode it for this ex.
var serverDate = '2015-06-26';
// Format it and save to vue data property
this.date = this.frontEndDateFormat(serverDate);
},
saveDataToServer: function() {
// Format data first before sending it back to server
var serverDate = this.backEndDateFormat(this.date);
// Ajax call sending formatted data (serverDate)
},
frontEndDateFormat: function(date) {
return moment(date, 'YYYY-MM-DD').format('DD/MM/YYYY');
},
backEndDateFormat: function(date) {
return moment(date, 'DD/MM/YYYY').format('YYYY-MM-DD');
}
}
});
This works well for me, hope it helps.
Here is a fiddle for it:
https://jsfiddle.net/crabbly/xoLwkog9/
Syntax UPDATE:
...
methods: {
getDataFromServer() {
// Ajax call to get data from server
// Let's pretend the received date data was saved in a variable (serverDate)
// We will hardcode it for this ex.
const serverDate = '2015-06-26'
// Format it and save to vue data property
this.date = this.frontEndDateFormat(serverDate)
},
saveDataToServer() {
// Format data first before sending it back to server
const serverDate = this.backEndDateFormat(this.date)
// Ajax call sending formatted data (serverDate)
},
frontEndDateFormat(date) {
return moment(date, 'YYYY-MM-DD').format('DD/MM/YYYY')
},
backEndDateFormat(date) {
return moment(date, 'DD/MM/YYYY').format('YYYY-MM-DD')
}
}
})
I had a similar problem when I wanted to uppercase an input value.
This is what I ended up doing:
// create a directive to transform the model value
Vue.directive('uppercase', {
twoWay: true, // this transformation applies back to the vm
bind: function () {
this.handler = function () {
this.set(this.el.value.toUpperCase());
}.bind(this);
this.el.addEventListener('input', this.handler);
},
unbind: function () {
this.el.removeEventListener('input', this.handler);
}
});
Then I can use this directive on the input field with a v-model.
<input type="text" v-model="someData" v-uppercase="someData">
Now, whenever I type into this field or change someData, the value is transformed to uppercase.
This essentially did the same thing as I hoped v-model="someData | uppercase" would do. But of course, you can't do that.
In summation: make a directive that transforms the data, not a filter.
Go to main.js and add the following code :
import moment from 'moment'
Vue.filter('myDate', function (value) {
if (value) {
return moment(String(value)).format('dd/mm/yyyy')
}
});
In your HTML do the following :
<label>Date</label>
<v-text-field :value="date | myDate" #input="value=>date=value"></v-text-field>
<p>{{ date | myDate 'dd/mm/yyyy' }}</p>
So we used above v-bind to bind the value and #input event handler to have the v-model functionality.
This is how I implemented a vue filter for a v-model using the watch callback, this won't update the value on load.
Vue.filter('uppercase', function (value) {
return value.toUpperCase();
});
The html:
<input type="text" v-model="someData">
And the watch callback:
watch:{
someData(val) {
this.someData = this.$options.filters.uppercase(val);
},
}
When you get the value initially, adjust it to fit the input. I got it working in the ready function, but you could do this after your DB call as well:
ready: function(){
var year = this.date.substr(0, 4);
var monDay = this.date.substr(5,5);
var result = monDay + "-" + year;
this.date = result.replace(/-/g,"/");
}
You may have to do something similar on the way back up to your database as well.
I would like to offer an alternative, with the hook update from Vue-directive
export default {
data() {
return {
someData: '',
}
},
directives: {
lowercase: {
update(el) {
el.value = el.value.toLowerCase()
},
},
uppercase: {
update(el) {
el.value = el.value.toUpperCase()
},
},
capitalize: {
update(el) {
const lowerCase = el.value.toLowerCase()
el.value = lowerCase.charAt(0).toUpperCase() + lowerCase.slice(1)
},
},
},
}
or
Vue.directive('lowercase', {
update(el) {
el.value = el.value.toLowerCase()
}
})
Vue.directive('uppercase', {
update(el) {
el.value = el.value.toUpperCase()
}
})
Vue.directive('capitalize', {
update(el) {
const lowerCase = el.value.toLowerCase()
el.value = lowerCase.charAt(0).toUpperCase() + lowerCase.slice(1)
}
})
<input type="text" v-model="someData" v-capitalize />
Template
<input type="text" v-model="date" #change="onDateChange">
Component
methods: {
onDateChange(event) {
this.myDate = event.target.value
}
getFormatedDate(date) {
return '2020/08/19'
}
},
computed: {
date() {
return this.getFormatedDate(this.myDate)
}
}
use value for bind on text field
example:
<v-text-field :value="string | stringFormat"></v-text-field>
I've found that I can filter input using the ordinary component v-bind:value / v-on:input dance without resorting to data or watch clauses if I just call $forceUpdate() after emitting the filtered value:
The Vue component:
{
props: ['value'],
methods: {
mEmit: function(EVT) {
const VAL = EVT.target.value;
var FILTERED = myFilterFunction(VAL);
this.$emit('input', FILTERED);
this.$forceUpdate();
}
}
}
The component HTML (data is filtered as it is entered):
<input type="text" v-bind:value="value" v-on:input="mEmit($event)" />
Using the component:
<my-component v-model="myDataVar"></my-component>

Categories