Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

'v-model' directives cannot update the iteration variable itself

I read an article about Renderless Components,it split a component into a presentational component (view part) and a renderless component(logical part) via $scopedSlots property.Here is a simple Tag component. when you press enter , you'll add a new tag

<!DOCTYPE html>
<html>
<head>
<script src="http://vuejs.org/js/vue.js"></script>
  <meta charset="utf-8">
  <title>JS Bin</title>
</head>
<body>
  <div id="app">
    <custom-component v-model="tags">
      <div slot-scope="{tags,addTag,newTag}">
        <span v-for="tag in tags">
          {{tag}}
        </span>
        <input type="text" @keydown.enter.prevent="addTag" v-model="newTag">
      </div>
    </custom-component>
  </div>

   <script>
     Vue.component('custom-component',{
        props:['value'],
        data(){
          return {
            newTag:''
          }
        },
        methods:{
          addTag(){
            this.$emit('input',[...this.value,this.newTag])      
            this.newTag = ''
          }
        },
        render(h){
          return this.$scopedSlots.default({
            tags:this.value,
            addTag:this.addTag,
            newTag:this.newTag
          })
        }
      })


      new Vue({
        el:'#app',
        data:{
        tags:[
         'Test',
         'Design'
         ]
        }
      })



   </script>
</body>
</html>

But,it doesn't work,it seems that the newTag always is ''(empty string), when I use SPA way, the emulator says "'v-model' directives cannot update the iteration variable 'newTag' itself" ,Here is demo on jsbin

The solution is , as mentioned in the article ,use :value attribute binding, and an @input event binding ,instead of v-model. demo on jsbin

<!DOCTYPE html>
<html>
<head>
<script src="http://vuejs.org/js/vue.js"></script>
  <meta charset="utf-8">
  <title>JS Bin</title>
</head>
<body>
  <div id="app">
    <custom-component v-model="tags">
      <div slot-scope="{tags,addTag,inputAttrs,inputEvents}">
        <span v-for="tag in tags">
          {{tag}}
        </span>
        <input type="text" v-bind="inputAttrs" v-on="inputEvents">
      </div>
    </custom-component>
  </div>

   <script>
     Vue.component('custom-component',{
       props:['value'],
       data(){
         return {
           newTag:''
         }
       },
       methods:{
         addTag(){
          this.$emit('input',[...this.value,this.newTag])
          this.newTag = ''
         }
       },
       render(h){
         return this.$scopedSlots.default({
           tags:this.value,
           addTag:this.addTag,
           inputAttrs:{
             value:this.newTag
           },
           inputEvents:{
             input:(e) => {
               this.newTag = e.target.value
             },
             keydown:(e) => {
               if(e.keyCode === 13){
               e.preventDefault()
               this.addTag()
           }
         }
        }
      })
     }
    })


    new Vue({
     el:'#app',
     data:{
       tags:[
        'Test',
        'Design'
       ]
     }
   })



   </script>
</body>
</html>

I don't know why v-model doesn't work.

EDIT

Questions above have been answered clearly, while I got another question after I read a reference link, and still v-model doesn't work question


<!DOCTYPE html>
<html>
<head>
<script src="http://vuejs.org/js/vue.js"></script>
  <meta charset="utf-8">
  <title>JS Bin</title>
</head>
<body>
<div id="app">
  <base-test v-slot="sp">
    <input type="text" v-model="sp.foo">
    <div>{{ sp}}</div>
  </base-test>
</div>
<script>
  Vue.component('base-test', {
  template: `
  <div>
    <slot :foo="foo"></slot>
  </div>
  `,
  data(){
    return{
      foo: 'Bar',
    }
  }
});


// Mount
new Vue({
  el: '#app',
});
</script>
</body>
</html>

As we can see, sp is an object. why v-model seems to be not working this time?

like image 842
Archsx Avatar asked Sep 17 '19 12:09

Archsx


3 Answers

My solution was very simple (see v-model="tags[index]"):

Instead of doing this:

<template v-for="tag in tags">
    <TagView :key="tag.key" v-model="tag" />
</template>

You should do this:

<template v-for="(tag, index) in tags">
    <TagView :key="tag.key" v-model="tags[index]" />
</template>

The reason is you cannot pass iterated object tag into v-model for modifications. Please find more info about this: Iterating a list of objects with foreach

like image 102
ADM-IT Avatar answered Oct 24 '22 00:10

ADM-IT


Consider the following two JavaScript examples:

for (let value of array) {
  value = 10
}
function (value) {
  value = 10
}

In both cases trying to assign 10 to value will only have an effect locally, it won't have any impact beyond the local scope. The caller, for example, would not be affected by the change.

Now consider these two examples where an object is used instead, where the object is of the form { value: 9 }:

for (let valueWrapper of array) {
  valueWrapper.value = 10
}
function (valueWrapper) {
  valueWrapper.value = 10
}

In this case the change is not limited to the local scope as we're updating objects. External code, such as the caller of the function, would also be impacted by this change to the value property as it can see the same object.

These examples are equivalent to trying to update a value using v-model in a variety of cases. The first two examples are equivalent to:

<template v-for="value in array">
  <input v-model="value">
</template>

and:

<template v-slot="{ value }">
  <input v-model="value">
</template>

The arguments passed to v-slot can very much be thought of as analogous to function parameters. Neither the loop nor the scoped slot would work as desired, exactly the same as they don't for their pure JavaScript equivalents.

However, the latter two of my four examples would be equivalent to:

<template v-for="valueWrapper in array">
  <input v-model="valueWrapper.value">
</template>

and:

<template v-slot="{ valueWrapper }">
  <input v-model="valueWrapper.value">
</template>

These should work fine as they are updating a property on an object.

However, to go back to the original question, it's important that we're binding the appropriate object. In this case we would need to bind the newTag property of the component. Copying that property to another object wouldn't work either as v-model would just be updating an irrelevant object.

like image 20
skirtle Avatar answered Oct 24 '22 01:10

skirtle


I think we shouldn't modify the passed data to a slot, pretty much like component props. However, I think it could be a bug.

1st approach

The v-model directive works using a nested field in the passed data to the slot.

<!DOCTYPE html>
<html>
<head>
<script src="http://vuejs.org/js/vue.js"></script>
  <meta charset="utf-8">
  <title>JS Bin</title>
</head>
<body>
  <div id="app">
    <custom-component v-model="tags">
      <div slot-scope="{tags,addTag,input}">
        <span v-for="tag in tags">
          {{tag}}
        </span>
        <input type="text" @keydown.enter.prevent="addTag" v-model="input.value">
      </div>
    </custom-component>
  </div>
</body>
</html>
Vue.component('custom-component',{
  props:['value'],
  data(){
    return {
      input: {
        value: ''
      }
    }
  },
  methods:{
    addTag(){
      this.$emit('input',[...this.value,this.input.value])
      console.log([...this.value,this.input.value])
      this.input.value = ''
    }
  },
  render(h){
    return this.$scopedSlots.default({
      tags:this.value,
      addTag:this.addTag,
      input:this.input
    })
  }
})


new Vue({
  el:'#app',
  data:{
    tags:[
      'Test',
      'Design'
    ]
  }
})

2nd approach

Use the input event to get the input value attribute directly

<!DOCTYPE html>
<html>
<head>
<script src="http://vuejs.org/js/vue.js"></script>
  <meta charset="utf-8">
  <title>JS Bin</title>
</head>
<body>
  <div id="app">
    <custom-component v-model="tags">
      <div slot-scope="{tags,addTag}">
        <span v-for="tag in tags">
          {{tag}}
        </span>
        <input type="text" @keydown.enter.prevent="addTag">
      </div>
    </custom-component>
  </div>
</body>
</html>
Vue.component('custom-component',{
  props:['value'],
  data(){
    return {
      newTag:''
    }
  },
  methods:{
    addTag(evt){
      console.log(evt.target.value)
       this.$emit('input',[...this.value, evt.target.value])
      evt.target.value = ''
    }
  },
  render(h){
    return this.$scopedSlots.default({
      tags:this.value,
      addTag:this.addTag,
      newTag:this.newTag
    })
  }
})


new Vue({
  el:'#app',
  data:{
    tags:[
      'Test',
      'Design'
    ]
  }
})

You can also check these related issues:

StackOverflow

Using v-model inside scoped slots

Issues and Forum

https://forum.vuejs.org/t/v-model-and-slots/17616

https://github.com/vuejs/vue/issues/9726

like image 2
Felipe Guizar Diaz Avatar answered Oct 24 '22 02:10

Felipe Guizar Diaz