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/

secretAgentB
  • 1,279
  • 4
  • 19
  • 36
  • Can you share live demo of this ? – Niklesh Raut Nov 13 '17 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. – hedin Nov 13 '17 at 17:35
  • 1
    By the way, did you saw this native vue-component-library: http://sagalbot.github.io/vue-select/ ? -- I used this for my project. It is already vue-component, so no wrapper needed. – hedin Nov 13 '17 at 17:39
  • Also, please mention this moment: you are using `v-for` for object without `:key`-attribute here: ` – hedin Nov 13 '17 at 17:46
  • On that note, it's compulsory in later versions of Vue to have the `:key` when using `v-for` – webnoob Nov 13 '17 at 17:58
  • Hi Alex, we didn't want to stray away for the standard elements that HTML provides because our backend is a ASP.NET MVC Core backend that uses Razor to create the html. When you use a component didn't you have to use a custom tag ... to get it to work? And we decided to not go with the TagHelper route in ASP.NET MVC. – secretAgentB Nov 13 '17 at 18:00
  • `>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.` Not exactly. The problem is not in that you passing object. The problem that you passing non-existing object in that scope. in this code: ` – hedin Nov 13 '17 at 21:30
  • Sorry I was not being clear there. I am not referring to the user in the v-for here – secretAgentB Nov 13 '17 at 21:36
  • As a suggestion to skip these challenges try this https://monterail.github.io/vue-multiselect/ – Yaser Khahani Nov 14 '17 at 06:30
  • 1
    You can use an HTML select element and set the `is` attribute to make it a select2 component. https://vuejs.org/v2/api/#is – Bert Nov 15 '17 at 15:52

2 Answers2

1

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.

hedin
  • 1,004
  • 10
  • 18
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>
tony19
  • 125,647
  • 18
  • 229
  • 307
Richard Matsen
  • 20,671
  • 3
  • 43
  • 77