Search code examples
javascriptvue.jsvuejs2jquery-select2vue-directives

VueJs directive two way binding


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/


Solution

  • 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.