vuejs / vue

This is the repo for Vue 2. For Vue 3, go to https://github.com/vuejs/core
http://v2.vuejs.org
MIT License
207.89k stars 33.69k forks source link

Dynamic v-model directive #1056

Closed karevn closed 9 years ago

karevn commented 9 years ago

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 commented 9 years ago

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

karevn commented 9 years ago

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 commented 9 years ago

+1 , i am looking for this too

misaka42 commented 9 years ago

+1, hope to see this

yyx990803 commented 9 years ago

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

karevn commented 9 years ago

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 commented 9 years ago

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

Pandahisham commented 9 years ago

we can ? wow !

Pandahisham commented 9 years ago

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

karevn commented 9 years ago

@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 commented 9 years ago

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 commented 8 years ago

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 commented 8 years ago

@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 commented 8 years ago

@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 commented 8 years ago

+1

victorwpbastos commented 8 years ago

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

bhoriuchi commented 8 years ago

@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 commented 8 years ago

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 commented 7 years ago

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

bhoriuchi commented 7 years ago

@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 commented 7 years ago

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

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

kamalkhan commented 7 years ago

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 commented 6 years ago

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 commented 6 years ago

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

praveenpuglia commented 6 years ago

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

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

praveenpuglia commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

@vielhuber try to use ref?

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

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 commented 6 years ago

This works for me.

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

danhanson commented 5 years ago

@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 commented 5 years ago

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

or just

`

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

njzydark commented 5 years ago
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 commented 5 years ago

@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 commented 5 years ago

@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 commented 5 years ago

@danhanson looks great, man

fritx commented 5 years ago

@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 commented 5 years ago

@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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

@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 commented 3 years ago

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 commented 2 years ago

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