2

I created a custom directive to handle select2 in VueJs. The code below works when I am binding a select to a data property in my viewmodel that is not a propert of an object within data.

Like this.userId but if it is bound to something like this.user.id, it would not update the value in my viewmodel data object.

Vue.directive('selected', {    
    bind: function (el, binding, vnode) {    
        var key = binding.expression;    
        var select = $(el);    

        select.select2();    
        vnode.context.$data[binding.expression] = select.val();    

        select.on('change', function () {    
            vnode.context.$data[binding.expression] = select.val();    
        });    
    },    
    update: function (el, binding, newVnode, oldVnode) {    
        var select = $(el);    
        select.val(binding.value).trigger('change');    
    }    
});

<select v-selected="userEditor.Id">
   <option v-for="user in users" v-bind:value="user.id" >
       {{ user.fullName}}
   </option>
</select>

Related fiddle: https://jsfiddle.net/raime910/rHm4e/4/

10
  • Can you share live demo of this ? Commented Nov 13, 2017 at 17:14
  • 1
    Why are you using directive instead of custom Vue-component? IMHO, component-way is match simpler -- you will not need to use this hack vnode.context.$data, also you can use created, mounted hooks, etc. Couple weeks ago I implemented wrapper for some jquery plugin in Vue: first time I went directive-way -- soon it became too complicated, so I refactored to separate vue-component and now it is simple and works fine. Commented Nov 13, 2017 at 17:35
  • 1
    By the way, did you saw this native vue-component-library: sagalbot.github.io/vue-select ? -- I used this for my project. It is already vue-component, so no wrapper needed. Commented Nov 13, 2017 at 17:39
  • Also, please mention this moment: you are using v-for for object without :key-attribute here: <option v-for="user in users" v-bind:value="user.id" >. It should be like that: <option v-for="user in users" v-bind:value="user.id" :key="user.id"> (see this documentation: vuejs.org/v2/guide/list.html#key). In short - if you are iterating objects, and not primitive types -- you probably will have unexpected errors with Vue-reactivity. (I spent full work day to figure out same omission in my project). Commented Nov 13, 2017 at 17:46
  • 1
    You can use an HTML select element and set the is attribute to make it a select2 component. vuejs.org/v2/api/#is Commented Nov 15, 2017 at 15:52

2 Answers 2

2
+50

When you using 1st level $data's-property, it accessing to $data object directly through []-brackets

But you want to pass to selected-directive the path to nested object, so you should do something like this:

// source: https://stackoverflow.com/a/6842900/8311719
function deepSet(obj, value, path) {
    var i;
    path = path.split('.');
    for (i = 0; i < path.length - 1; i++)
        obj = obj[path[i]];

    obj[path[i]] = value;
}

Vue.directive('selected', {    
bind: function (el, binding, vnode) {    
    var select = $(el);    

    select.select2();    
    deepSet(vnode.context.$data, select.val(), binding.expression);    

    select.on('change', function () {    
        deepSet(vnode.context.$data, select.val(), binding.expression);
    });    
},    
update: function (el, binding, newVnode, oldVnode) {    
    var select = $(el);    
    select.val(binding.value).trigger('change');    
}    
});

<select v-selected="userEditor.Id">
<option v-for="user in users" v-bind:value="user.id" >
   {{ user.fullName}}
</option>
</select>

Description:

Suppose we have two $data's props: valOrObjectWithoutNesting and objLvl1:

data: function(){
  return{
    valOrObjectWithoutNesting: 'let it be some string',
    objLvl1:{
      objLvl2:{
        objLvl3:{
          objField: 'primitive string'
        }
      }
    }
  }
}

Variant with 1st level $data's-property:

<select v-selected="valOrObjectWithoutNesting">

// Now this code:
vnode.context.$data[binding.expression] = select.val();
// Equals to: 
vnode.context.$data['valOrObjectWithoutNesting'] = select.val();

Variant with 4th level $data's-property:

<select v-selected="objLvl1.objLvl2.objLvl3.objField">

// Now this code:
vnode.context.$data[binding.expression] = select.val();
// Equals to: 
vnode.context.$data['objLvl1.objLvl2.objLvl3.objField'] = select.val(); // error here

So the deepSet function in my code above "converting" $data['objLvl1.objLvl2.objLvl3.objField'] to $data['objLvl1']['objLvl2']['objLvl3']['objField'].

As you see, as I mentioned in comments to your question, when you want make select2-wrapper more customisable, the directive-way much more complicated, than separate component-way. In component, you would pass as much configuration props and event subscriptions as you want, you would avoid doing side mutations like vnode.context.$data[binding.expression] and your code would become more understandable and simpler for further support.

Sign up to request clarification or add additional context in comments.

Comments

0

A custom directive is perfectly fine, except use the insertedhook instead of bind. Adapted from Vue Wrapper Component Example.

To bind to an object property, the simplest way is to wrap it in a computed setter Computed Setter and bind to that.

Note, 'deep setting' does not appear to work. The problem is one of change detection, which the computed setter overcomes. (Note that the on('change' function is jQuery not Vue.)

console.clear()

Vue.directive('selected', {
  inserted: function (el, binding, vnode) {
    var select = $(el);
    select
      .select2()
      .val(binding.value)
      .trigger('change')
      .on('change', function () {
        if (vnode.context[binding.expression]) {
          vnode.context[binding.expression] = select.val();     
        }
      })
    },
});

var vm = new Vue({
  el: '#my-app',
  computed: {
    selectedValue: {
      get: function() { return this.myObj.type },
      set: function (value) { this.myObj.type = value }
    }
  },
  data: {
    selectedVal: 0,
    myObj: { type: 3 },
    opts: [{
      id: 1,
      text: 'Test 1'
    }, {
      id: 2,
      text: 'Test 2'
    }, {
      id: 3,
      text: 'Test 3'
    }]
  }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.5/css/select2.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.5/js/select2.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.4/vue.js"></script>

<div id="my-app">
  <div>
    <label for="example">Test dropdown list ({{ myObj.type }})</label>
  </div>
  <div>
    <select id="example" style="width: 300px" v-selected="selectedValue">
      <option v-for="(opt,index) in opts" :value="opt.id" :key="index">
        {{ opt.text }}
      </option>
    </select>
  </div>
</div>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.