The 'v-model' directive is unable to modify the iteration variable directly

I recently came across an interesting article about Renderless Components, which discuss splitting a component into a presentational (view) part and a renderless (logical) part using the $scopedSlots property. The concept was demonstrated with a simple Tag component that allows you to add a new tag when you press enter.

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

However, I encountered a problem where the newTag always remains an empty string. The error message suggested that 'v-model' cannot update the iteration variable 'newTag'. You can view a live demo here.

The solution proposed in the article is to use :value attribute binding and an @input event binding instead of v-model. You can see an updated demo on JSBin for reference.

<!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'm still puzzled as to why v-model isn't working in my case.

EDIT

While previous questions have been answered, I have encountered another issue related to v-model not functioning as expected. It seems to be a problem with 'sp.foo' being assigned as an object.


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

If anyone has insights on how to resolve the v-model issue in this scenario, please feel free to share your thoughts.

Answer №1

It's quite simple how I tackled this issue (check out v-model="tags[index]"):

Rather than approaching it like this:

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

You should do it this way:

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

The logic behind this is that you can't pass the iterated object tag directly into v-model for any potential changes. For more information on this, check out: Iterating a list of objects with foreach

Answer №2

Let's explore two scenarios in JavaScript:

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

In both cases, changing the value to 10 will only affect the local scope and not have any impact outside of it. This means that the caller would remain unaffected by the change.

Now, consider a different approach using objects with a structure like { value: 9 }:

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

With this object-oriented method, the changes are not limited to the local scope as the objects get updated. Any external code, such as the caller, would also see the change in the value property since they share the same object.

Comparing these examples to updating values using v-model, some equivalents can be drawn. The first set of examples correspond 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 be likened to function parameters. Just like their JavaScript counterparts, neither the loop nor the scoped slot will behave as desired.

However, the second set of examples would equate 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 properly since they are updating a property on an object.

Returning to the original question, it is vital to bind the correct object - in this case, the newTag property of the component. Copying this property to another object would not suffice as v-model would then be updating an unrelated object.

Answer №3

In my opinion, it's not a good practice to alter the data passed to a slot, similar to how we handle component props. However, I suspect this behavior could be a bug.

Initial Thought

The v-model directive functions by using a nested field within the data passed 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'
    ]
  }
})

Alternative Approach

Another way would be to utilize the input event directly to extract the input value attribute.

<!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'
    ]
  }
})

For more information and discussions on similar topics, you can refer to:

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

Answer №4

Utilize the Ref of Vuejs Object in your solution, incorporating reference and testing with tailwind, vue3, as a standalone component.

Please don't forget to rate this, it greatly helps me!

// Utilizing ref function to extend object properties
// Importing ref function from Vue for extending object properties
import { ref } from 'vue';

export default {
  data() {
    return {
      // Creating references to objects that will change
      fruits: ref({
        oranges: {
          name: 'naranjas',
          value: false
        },
        apple: {
          name: 'manzanas',
          value: false
        },
        pear: {
          name: 'peras',
          value: false
        }
      })
    };
  }
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<template>
  <div>
    <!-- Using tailwindcss for Vue styling -->
    <div class="w-2xl bg-blue-50 text-gray-400 font-bold">
      <!-- Iterating through fruits array -->
      <div v-for="fruit in fruits" :key="fruit">
        <label for="">{{ fruit.name }}</label>
        <input v-model="fruit.value" type="checkbox">
      </div>
    </div>
    <div class="w-2xl bg-blue-50 text-gray-400 font-bold">
      {{fruits}}
    </div>
  </div>
</template>

I've experimented with various methods to modify the v-model without success, ultimately resorting to referencing the JavaScript object directly.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Creating form components in a row

I'm currently working on an app and I'm attempting to align 3 form elements in a ratio of 25/25/50, but it's proving to be quite challenging. At the end of this message, you'll find a picture showing the current layout. Additionally, I& ...

SyntaxError: End of input caught unexpectedly (NodeJS TCP server)

My simple tcp server has the capability to send and receive json data. Here is a snippet of my code: // Handling incoming messages from clients. socket.on('data', function (data) { var obj = JSON.parse(data); if(obj.type == "regis ...

I am experiencing difficulty with implementing a page break within the <tr> and <tbody> elements

Below is the code I am working with: {% for item in child.items %} {%set ns.count = ns.count + 1%} <tbody style="page-break-inside: avoid"> <tr style="page-break-inside: avoid"> ...

What causes the discrepancy between the display order of JSON.stringify and array iteration?

Currently, I am in the process of developing a commenting system where users have the ability to sort comments based on the number of upvotes they receive. My approach involves using PHP to handle the sorting logic and then transferring the sorted data to ...

I am having trouble establishing a connection to the JavaScript MQTT server

Error Encountered: WebSocket Error 12031 - The Server Connection Was Reset In order to subscribe to MQTT messages from the user interface, the code below is being utilized. A Mosquitto broker is currently running on my local machine, with the IP address s ...

Getting the dimensions of an image when clicking on a link

Trying to retrieve width and height of an image from this link. <a id="CloudThumb_id_1" class="cloud-zoom-gallery" rel="useZoom: 'zoom1', smallImage: 'http://www.example.com/598441_l2.jpg'" onclick="return theFunction();" href="http ...

Unable to render Blender model in threeJS scene due to undefined condition

I've tried following solutions from Stack Overflow to resolve this issue, but I'm still unable to load external objects (Blender). Essentially, I exported it as a ThreeJS JSON file, here is the content of my JSON file: { "textures":[], ...

Exploring Azure: Obtain a comprehensive list of application settings from a deployed Node.js web application

After successfully deploying a NodeJs app to a Linux Azure AppService, I am now aiming to retrieve the server settings of this particular app-service. By enabling managed Identity for the AppService under the 'Identity' tab, I attempted to achiev ...

Employ useEffect with numerous dependencies

I am currently working on fetching employee data using the useEffect hook. function AdminEmployees() { const navigate = useNavigate(); const dispatch = useDispatch(); // Fetching employee data const { adminEmployees, loading } = useSelector( ( ...

Issue with Bootstrap Table Style When Using window.print();

The color of my Bootstrap table style is not displaying correctly in the print preview using window.print(). Here is a screenshot showing that the table style is not working properly: https://i.stack.imgur.com/eyxjl.jpg Below is the code I am using: < ...

Guide on initiating document-wide events using Jasmine tests in Angular 2/4

As stated in the Angular Testing guidelines, triggering events from tests requires using the triggerEventHandler() method on the debug element. This method accepts the event name and the object. It is effective when adding events with HostListener, such as ...

How can I categorize the message.author.id into different arrays according to numerical values?

There's a coding scenario that involves inserting the message.author.id into an array within a JSON file. The code snippet looks like this: if(message.content.startsWith (prefix + " craft")) { let editedmessage = message.content.slice(pr ...

Issue with if statement when checking element.checked

Hey everyone, I'm currently working on a calculator app and running into an issue. I have set up 3 radio buttons and I would like to check them using an 'if statement' in my JS file. However, the problem is that the 'main' element ...

Combining various datasets with identical X values in a D3 bar graph

I'm currently working on creating a grouped bar chart to display performance test results using D3 for the first time. The X axis should represent parallelism, indicating the number of threads used, while the Y axis will show the duration in millisec ...

Encountering an issue with usememo in React js?

I'm currently experimenting with the useMemo hook in React JS. The goal is to sort an array of strings within a function. However, when I return the array from the function, only the first element is being returned. Can someone please assist me in ide ...

Using jQuery to append text after multiple element values

I currently have price span tags displayed on my website: <div> <span class="priceTitle">10.00</span> </div> <div> <span class="priceTitle">15.00</span> </div> <div> <span class="priceTitle">20.0 ...

Tips for telling the difference between typescript Index signatures and JavaScript computed property names

ngOnChanges(changes: {[paramName: string]: SimpleChange}): void { console.log('Any modifications involved', changes); } I'm scratching my head over the purpose of 'changes: {[propName: string]: SimpleChange}'. Can someone cl ...

Using PHP to populate Highcharts with data through the use of json_encode

I've been grappling with integrating highcharts and json_encode using PHP for a few days now. I'm confident that my data is correctly formatted, yet the chart isn't updating as expected. The category data updates smoothly, and the series dat ...

Every time I attempt to compile NodeJS, I encounter a compilation error

Within mymodule.js var fs = require('fs') var path = require('path') module.exports = function(dir, extension, callback){ fs.readdir(dir, function(error, files){ if(error) return callback(error) else { ...

Issue with Bootstrap error class not functioning in conjunction with hide class

I need to create a form that slides down two input fields when an error occurs, and I want them to have the bootstrap error class. The 'error' class works fine without the 'hide' class being present, but when the 'hide' class ...