Vue: Dynamic v-model directive

Created on 16 Jul 2015  ·  42Comments  ·  Source: vuejs/vue

Currently, v-model directive does not support mustache-type bindings for binding expressions, but this feature would be extremely helpful for creating form builder-like implemenetations.

Most helpful comment

you can already do that with v-model="form[field.name]".

All 42 comments

Can you give an example of what it would be helpful for?

Let's, say, imagine we're building a clone of phpmyadmin, which receives data from DESCRIBE TABLE statement and builds row editor form from that data. Binding expressions will be inherently dynamic in this case, as we'll only know field names after running SQL DESCRIBE TABLE.

+1 , i am looking for this too

+1, hope to see this

I still don't fully understand what this enables that the current syntax cannot achieve. Maybe some code samples?

Some pseudo-code related to phpmyadmin clone described above:

    <script>
    modue.exports = {
        data: function(){
            //returns table structure pulled from the backend somehow
            return {fields: [
                {name: "id", type: "integer"},
                {name: "name", type: "varchar"},
                {name: "gender", type: "varchar"}
            ], 
            // this was initialised based on the structure above, does not matter how.
            form: {id: null, name: null, gender: null}); 
        },
       methods: {
          getBindingExpr: function(field){ /* blah-blah */ }
       }

    }
    </script>
    <template>
       <div v-repeat="field: fields">
          <!-- Here we need to display an editor bound to the field -->
           <input type="text" v-model="form.{{field.name}}">
        <!-- Or, we can call a function that calculates the binding expression --
          <input type="text" v-model="{{getBindingExpr(field)}}">
      </div>
    </template>

you can already do that with v-model="form[field.name]".

we can ? wow !

evan can you put up a js fiddle showing a todo-ish example

@yyx990803, that's great, but it was just an example showing just one example of dynamic usage. The logic might be more complex in some kind of a business application.

Just to be clear I'm against the idea of allowing interpolations inside directive expressions. Right now mustaches means the evaluated result is expected to be a string and used as a string (to be inserted into the DOM, or do a ID lookup). Evaluating mustaches into expressions which then can be evaluated makes it two layers of abstraction and can end up making your templates very confusing.

i think it would be very valuable to add the ability to interpolate the string before evaluating the expression

the data[pathString] method works well for objects with 1 nested level but for 2 or more i have not found a way to bind dynamically.

maybe add a modifier to the binding so that is it more clear than mustaches

Example

let myData = {}
let varPath = 'myData.path.to["my"].obj'
let modelPath = 'myData.path.to["my"].model'
<component-name :myparam.interpolate='varPath'></component-name>
<input v-model.interpolate='modelPath'>

or maybe a getter/setter function that can be passed.

disclaimer: i have not read the 2.0 spec so you may have addressed this there.

@bhoriuchi why no computed property?

computed: {
  varPath: function() {
    return this.myData.path.to['my'].obj;
  },
},
<component-name :myparam="varPath"></component-name>

And for v-model you can use computed property with setter.

@simplesmiler i have not tried computed properties in a two-way binding, ill give it a shot. thanks for the tip.

Update

@simplesmiler - so the issue i am running into with using a computed property is that i have no way to pass arguments to the computed property. this inside the getter or even value in get(value) both point to the component.

some background on my use case.

i am creating a form builder that uses a json object to build the forms. the config object is more or less a 2 dimensional array of objects (rows/forms). each form config object has a model field that has the path string to the field that should be set. in order to use a computed property for this i would need to be able to determine from the component using the component binding what row/form index in order to look up the model path from the config object

currently i have this working using a pre-initialized 2 dimensional array called formData that i bind each form model to with v-model="formData[rowIndex][formIndex]" and i watch that object for changes and update the parent data object, but i dislike this approach because it requires me to preinitialize an array for dynamic field addition.

i need 2 levels of nesting because i am using this form builder component on another component that needs to set an object that looks something like

data: {
  templates: {
    operatingSystems: {
      <someuuid1>: [ <osid1>, <osid2> ],
      <someuuid2>: [ <osid5>, <osid10>, <osid22> ]
   }
  }
}

where my path string would look like

templates.operatingSystems[<dynamic uuid>]

Update 2

i changed from using a multi-dimensional array to a plain object with key names

"<rowIndex>_<formIndex>"

and used a deep watch to keep the data in sync with the parent. I still think an interoplated bind would be beneficial.

+1

For me, v-model="$data[field.name]" does the trick!

@victorwpbastos this does not work for setting deeply nested objects as it will just use the field.name as the key

for example if you have the following data and field string

$data = {
  'animal': {
    'dog': {
      'husky': 1
    }
  }
}
field.name = 'animal.dog.husky'

and you use

v-model="$data[field.name]"

and enter the value of 2 on the form, the data would end up looking like this

$data = {
  'animal': {
    'dog': {
      'husky': 1
    }
  },
 'animal.dog.husky': 2
}

the reason interpolated bind is useful is where you are building dynamic nested inputs where you cant "hard code" the parent path (e.g 'animal.dog') into the directive

I revisited this found a more simple solution. You can create a custom object and add getters/setters to it on created using the model path string. Here is a simple example

input-list

<template lang="jade">
  div
    div(v-for="form in config.forms")
      input(v-model="formData[form.model]")
</template>

<script type="text/babel">
  import Vue from 'vue'
  import _ from 'lodash'

  export default {
    props: ['value', 'config'],
    computed: {},
    methods: {
      vueSet (obj, path, val) {
        let value = obj
        let fields = _.isArray(path) ? path : _.toPath(path)
        for (let f in fields) {
          let idx = Number(f)
          let p = fields[idx]
          if (idx === fields.length - 1) Vue.set(value, p, val)
          else if (!value[p]) Vue.set(value, p, _.isNumber(p) ? [] : {})
          value = value[p]
        }
      }
    },
    data () {
      return {
        formData: {}
      }
    },
    created () {
      _.forEach(this.config.forms, (form) => {
        Object.defineProperty(this.formData, form.model, {
          get: () => _.get(this.value, form.model),
          set: (v) => this.vueSet(this.value, form.model, v)
        })
      })
    }
  }
</script>

in use

<template lang="jade">
  div
    input-list(v-model="formData", :config='formConfig')
</template>

<script type="text/babel">
  import InputList from './InputList'
  export default {
    components: {
      InputList
    },
    data () {
      return {
        formData: {
          name: 'Jon',
          loc: {
            id: 1
          }
        },
        formConfig: {
          forms: [
            { type: 'input', model: 'loc.id' },
            { type: 'input', model: 'loc["name"]' }
          ]
        }
      }
    }
  }
</script>

If using this way, any way we can set the watcher for each of the reactive data created dynamically?

@luqmanrom I am not familiar with the inner workings of the vue watcher but I believe anything created with vue.set can be watched so you could add some code to watch dynamic props and emit evens on changes or you can seep watch the target object. Someone else might have a better suggestion

I wrote a toolkit for this. also allows you to mutate vuex using v-model

https://github.com/bhoriuchi/vue-deepset

This should do the trick:

Directive

Vue.directive('deep-model', {
    bind(el, binding, vnode) {
        el.addEventListener('input', e => {
            new Function('obj', 'v', `obj.${binding.value} = v`)(vnode.context.$data, e.target.value);
        });
    },
    unbind(el) {
        el.removeEventListener('input');
    },
    inserted(el, binding, vnode) {
        el.value = new Function('obj', `return obj.${binding.value}`)(vnode.context.$data);
    },
    update(el, binding, vnode) {
        el.value = new Function('obj', `return obj.${binding.value}`)(vnode.context.$data);
    }
});

Usage (Component)

const component = Vue.extend({
    template: `<input v-deep-model="'one.two.three'">`,
    data() {
        return {
            one: { two: { three: 'foo' } }
        };
    }
});

Here is the Gist Reference.

Hi any body here. I am using VUE.js with Laravel. I have Dynamic Custom Form fields coming from the database. I followed @yyx990803 . v-model="form['name']". The field works. But the problem is i can not get the field values in laravel Controller. Anybody here. I am using @tylerOtwell Form.js Class.
your help will be greatly appreciated.
Thanks

This is not a help forum. We have one dedicated for answering questions at https://forum.vuejs.org

I really struggled trying to have a function invoked to find out v-model value. Here's an example.

I am trying to build a date range picker which looks like this.
image

Here, the presets are coming from an array that looks like this..

presets = [
  {
    label: 'Today',
    range: [moment().format('YYYY-MM-DD'), moment().format('YYYY-MM-DD')]
  },
]

Now, I also have two dates for those input fields in my data of component. startDate & endDate.

What I really want to do is compare the date user has selected with the dates passed in my preset configuration and set the v-model value to either true or false but I am unable because...

  • v-model doesn't accept conditions so I can't do preset.range[0] === startDate && preset.range[1] === endDate.
  • v-model doesn't allow v-for alias being passed to a function. So I can't do something like
<li v-model="isActive(index)" v-for="(preset, index) in presets">
...
</li>

Allowing to have conditional statements at the least can solve this problem easily.

Also, I may be doing something fundamentally wrong so please point out if I could achieve it in any different way.

Currently I have solved the problem by exploiting the fact that computed properties are function calls.

Script

computed: {
  isActive() {
      return this.presets.map(
        preset =>
          preset.range[0] === this.startDate && preset.range[1] === this.endDate
      );
    }
}

Template

<li v-model="isActive[index]" v-for="(preset, index) in presets">
...
</li>

But it really seems like a hack to me. Not sure. Please suggest.

Does anybody know if this also works in combination with Vuex as explained here? https://vuex.vuejs.org/guide/forms.html

I want to have an input field which is a little bit dynamic.

<input v-model="dataHandler" :scope="foo" type="checkbox" />

How can I access "scope" of the dom element inside the following code?

computed: {
  message: {
    get () {
      //
    },
    set (value) {
      //
    }
  }
}

@vielhuber try to use ref?

<input ref="myInput" v-model="dataHandler" :scope="foo" type="checkbox" />
this.$refs.myInput.getAttribute('scope') // => 'foo'

Hi, I have a Vue question related to this topic - "dynamic v-model directive":

When I'm implementing a Vue component, how can I dynamically control the v-model modifier - .lazy, etc??
for example:

<el-input v-model[field.lazy ? '.lazy' : '']="model[field.key]">

This works for me.

<input v-model="$data[field].key" type="text">

@fritx To "dynamically" change the modifier, I used the v-if director like this.

<input v-if="field.lazy" v-model.lazy="model[field.key]">
<input v-else v-model="model[field.key]">

This can get cumbersome though if you want large variety of multiple combinations of modifiers.

I guess one option could be to create a reusable component that contains all the if statements and pass it the input component you want to render and the array of modifiers that determines which input with the desired modifiers is rendered. Using the if statement like above though was good enough for me.

I could not find the way for dynamically accessing computed property in v-model directive.
There is no way for me to access my computed properties as you can access data properties with
v-model="$data[something]"

My code is something like this:

computed: { get () { // }, set (value) { // } } }

I need the way to access computed property with string, which i couldn't find.
This is an example but different solutions would work as well.
<input v-model="$computed[someDynamicString]">
or just
<input v-model="[someDynamicString]">

The closest thing I have found is "_computedWatchers[someDynamicString].value" but that does not work with setters and getters, maybe it would work if it was just a computed value.

v-model="dialogTemp.tmBasFuelEntities[dialogTemp.tmBasFuelEntities.findIndex(t=>t.paramCode==item.paramCode)].paramValue"

This is my dialogTemp:

dialogTemp: {
  tmBasFuelEntities: [
    {
      paramCode: '',
      paramValue: ''
    },
    {
      paramCode: '',
      paramValue: ''
    },
    {
      paramCode: '',
      paramValue: ''
    },
  ]
}

@fritx To "dynamically" change the modifier, I used the v-if director like this.

<input v-if="field.lazy" v-model.lazy="model[field.key]">
<input v-else v-model="model[field.key]">

This can get cumbersome though if you want large variety of multiple combinations of modifiers.

I guess one option could be to create a reusable component that contains all the if statements and pass it the input component you want to render and the array of modifiers that determines which input with the desired modifiers is rendered. Using the if statement like above though was good enough for me.

It's cool but I had to pass lots of props to the very one which is so verbose, any idea? @danhanson

<template v-else-if="itemCom">
        <component v-if="getFieldType(field) === 'number'"
          :is="itemCom"
          :model="model"
          :field="field"
          :schema="schema"
          v-model.number="model[field.key]"
          v-loading="field.loading"
          v-bind="getFieldAttrs(field)"
          v-on="field.listen"
          @form-emit="handleFormEmit"
        ></component>
        <component v-else
          :is="itemCom"
          :model="model"
          :field="field"
          :schema="schema"
          v-model="model[field.key]"
          v-loading="field.loading"
          v-bind="getFieldAttrs(field)"
          v-on="field.listen"
          @form-emit="handleFormEmit"
        ></component>

@fritx You could change v-model to :value/@input and parse it manually.

</template>
        <component v-if="getFieldType(field) === 'number'"
          :is="itemCom"
          :model="model"
          :field="field"
          :schema="schema"
          :value="parseField(field, model[field.key])"
          @input="model[field.key] = parseField(field, $event.target.value)"
          v-loading="field.loading"
          v-bind="getFieldAttrs(field)"
          v-on="field.listen"
          @form-emit="handleFormEmit"
        ></component>
<template>
<script>

export default {
    ...
    methods: {
        parseField (field, val) {
            if (this.getFieldType(field) === 'number') {
                return Number(val);
            }
            return val;
        }
    }
};
</script>

@danhanson looks great, man

@danhanson I'm afraid it should be:

:value="getFieldValue(field, model[field.key])"
@input="model[field.key] = getFieldValue(field, $event)"
@change="model[field.key] = getFieldValue(field, $event)"

I'm not sure, I'll try. Thanks!

@ninojovic

I found a solution here: https://forum.vuejs.org/t/accessing-computed-properties-from-template-dynamically/4798/9

<input v-model="_self[someDynamicString]">
works for me

Something like this

<el-input
  v-if="!nestedField.widget"
  v-model="form[nestedField.id]"
  placeholder=""
  v-bind="nestedField.rest"
>
[
  {
    label: '收房价格',
    id: 'housePrice',
    type: Number,
    widget: 'div',
    fieldSet: [
      {
        label: '',
        id: 'housePrice',
        type: Number,
        defaultValue: 0,
        rest: {
          style: 'width:5em;'
        },
      },
      {
        label: '',
        id: 'priceUnit',
        type: String,
        widget: 'select',
        defaultValue: '元/月',
        options: [
          { label: '元/月', value: '元/月' },
          { label: '元/年', value: '元/年' },
          { label: ' 元/天·m2', value: '元/天·m2' },
        ],
        rest: {
          style: 'width:6em;'
        },
      },
    ],
  },
]

When field type is Number, I want to use v-model.number, which is much more convenient. @fritx

I teardown v-model to fit it.

<el-input
  v-if="!nestedField.widget"
  :value="form[nestedField.id]"
  @input="v => { form[nestedField.id] = isNumber(nestedField.type) ? Number(v) : v }"
  placeholder=""
  v-bind="nestedField.rest"
>
  <template v-if="nestedField.suffixText" slot="append">{{nestedField.suffixText}}</template>
</el-input>

I Have clone HMTL using (some part for form-input) which i am inserting using jquery. (don't say why i am using jquery). now my element is being inserted by jquery. so is it possible to bind v-model.

$('.area').append('formPart')
in form i have some inputs like
<div class="form-group">
<input type="text" name="area2" /> 
<input type="text" name="area3" />
</div>

So how i can bind v-model on area 2 and 3.

@ninojovic

I found a solution here: https://forum.vuejs.org/t/accessing-computed-properties-from-template-dynamically/4798/9

<input v-model="_self[someDynamicString]">
works for me

Works for me too, but the "_self" variable is reserved for Vue's internal properties (see #2098).

In other words, this implementation can breaking in the future.

I prefer this way:

<template>
  <input v-model="mySelf[someDynamicString]">
</template>

<script>
export default {
  data() {
    return {
      mySelf: this
    }
  }
}
</script>

For more details see: https://stackoverflow.com/questions/52104176/use-of-self-attribute-from-vue-vm-is-reliable

Was this page helpful?
0 / 5 - 0 ratings

Related issues

loki0609 picture loki0609  ·  3Comments

lmnsg picture lmnsg  ·  3Comments

bfis picture bfis  ·  3Comments

franciscolourenco picture franciscolourenco  ·  3Comments

paulpflug picture paulpflug  ·  3Comments