Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic v-model directive #1056

Closed
karevn opened this issue Jul 16, 2015 · 44 comments
Closed

Dynamic v-model directive #1056

karevn opened this issue Jul 16, 2015 · 44 comments

Comments

@karevn
Copy link

karevn commented Jul 16, 2015

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.

@yyx990803
Copy link
Member

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

@karevn
Copy link
Author

karevn commented Jul 16, 2015

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.

@Pandahisham
Copy link

+1 , i am looking for this too

@misaka42
Copy link

+1, hope to see this

@yyx990803
Copy link
Member

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

@karevn
Copy link
Author

karevn commented Jul 17, 2015

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>

@yyx990803
Copy link
Member

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

@Pandahisham
Copy link

we can ? wow !

@Pandahisham
Copy link

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

@karevn
Copy link
Author

karevn commented Jul 17, 2015

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

@yyx990803
Copy link
Member

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.

@bhoriuchi
Copy link

bhoriuchi commented Jun 20, 2016

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.

@simplesmiler
Copy link
Member

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

@bhoriuchi
Copy link

bhoriuchi commented Jun 20, 2016

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

@cve
Copy link

cve commented Jun 28, 2016

+1

@victorwpbastos
Copy link

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

@bhoriuchi
Copy link

bhoriuchi commented Jul 6, 2016

@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

@bhoriuchi
Copy link

bhoriuchi commented Oct 9, 2016

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>

@luqmanrom
Copy link

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

@bhoriuchi
Copy link

@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

@bhoriuchi
Copy link

bhoriuchi commented Feb 26, 2017

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

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

@kamalkhan
Copy link

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.

@amjadkhan896
Copy link

amjadkhan896 commented Jan 23, 2018

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

@LinusBorg
Copy link
Member

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

@praveenpuglia
Copy link

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.

@praveenpuglia
Copy link

praveenpuglia commented Feb 2, 2018

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.

@vielhuber
Copy link

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) {
      //
    }
  }
}

@fritx
Copy link

fritx commented Aug 24, 2018

@vielhuber try to use ref?

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

@fritx
Copy link

fritx commented Aug 24, 2018

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]">

@Clinsmann
Copy link

Clinsmann commented Oct 24, 2018

This works for me.

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

@danhanson
Copy link

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

@ninojovic
Copy link

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: { [someDynamicString]: { 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.

@njzydark
Copy link

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
Copy link

fritx commented Apr 1, 2019

@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>

@danhanson
Copy link

@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>

@fritx
Copy link

fritx commented Apr 8, 2019

@danhanson looks great, man

@fritx
Copy link

fritx commented Apr 8, 2019

@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!

@Summerdx
Copy link

@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

@wisetc
Copy link

wisetc commented Jun 21, 2019

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

@wisetc
Copy link

wisetc commented Jun 21, 2019

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>

@sudhir600
Copy link

sudhir600 commented Jul 5, 2019

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.

@gabrielwillemann
Copy link

gabrielwillemann commented Aug 13, 2019

@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

@Harinetha
Copy link

Harinetha commented May 6, 2021

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

      <div v-for="(item, index) in ruleForm.taskList" :key="index">
        <el-form-item
          :prop="'taskList.' + index + '.task'"
          :rules="{
            required: true,
            message: 'Task is required',
            trigger: 'blur',
          }"
        >
          <el-input placeholder="Enter Task" v-model="ruleForm.task"
            ><i
              class="el-icon-delete el-input__icon"
              slot="suffix"
              @click="remove(index)"
            >
            </i>
          </el-input>
        </el-form-item>
      </div>

 server data binding correctly but not able to edit the input, please help me with this?

@isuke01
Copy link

isuke01 commented Jan 21, 2022

I know this may be late.
But if ome one want the solutin for the dynamic form with dynamic binding + custom inputs component

First start with component
my-input my custom input handler

// Template
<div class="my-input-wrap">
        <input 
            :value="value" 
            @input="$emit('input', $event.target.value)"
       />
</div>

// The component script
export default {
    props: {
        value: '',
    }
}

my-form Component

gFields Is just an object with list of the fields, each filed have its own IDin it and default value and other info for my case.

//Template
<form v-if="gFields" class="my-form" >
      <template v-for="field in gFields" :class="field.cssClass" >
           <my-input 
                      :key="field.id"  
                      v-model="inputFields['input_'+field.id]" 
            />
            <!-- Just since we did the trick in component we can just use standard v-model --->
       </template>
</form>

// Now we have to make this input binded field reactivite
// https://vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
// We will use the method `Vue.set(object, propertyName, value)`
// The component script
export default {
    data(){
        return{
            gFields: null
            inputFields: {} //this is not yet reactivite
         }
    },
   methods: {
      async getMyForm(){
          //Just use any of you favorite way to get the form info
          await this.$axios.$get(baseUrl).then( form => { //request to your dynamic form 
              if(form.fields && form.fields.length){ // check for my fields
                        form.fields.forEach( f => { //loop for my fields
                             const val = f.defaultValue || '' // here I'm setting default value for my field
                             const inputKey = 'input_'+f.id  // Here I'm setting key  for my field
                             this.$set(this.inputFields, inputKey, val);  // This is the important part where we set the fields to be reactivite
                        })
                        this.gFields= form.fields // just save my fields some where or do it whatever you like it.
                })
              })
          }).catch( e => { console.warn('err', e ) } ) //just error handle
       }
   }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests