Mixing template v-for and v-models

Hi all, I am new to Vue, trying to create dynamic form which adds a new row of 2 input controls every time the last row has been fully set up. I am using vue@3.2.37 without typescript, vue/cli-service@5.0.4 and webpack.

I am using a v-for loop over a template, and v-model inside that… and getting out of my depth :slight_smile: I am posting here because my last attempt triggers a “This is likely a Vue internals bug.” message - though I am pretty sure I am not doing every right…

The component code looks like this:

<template>

  <form role="form" class="text-start" @submit.prevent="onSubmit">
    <template v-for="(dayRow, dayIndex) in dayRows" :key="dayIndex">
      <v-select :options="days" v-model="dayRow.day.value"
                @option:selected="checkDays" @option:deselected="checkDays"
                :id="'day' + dayIndex" name="'day' + dayIndex"/>
      <time-picker v-model="dayRow.time.value" :minute-interval="5" close-on-complete manual-input
                   hour-label="heure" minute-label="mn" auto-scroll
                   :id="'time' + dayIndex" name="'time' + dayIndex"
                   fixed-dropdown-button hide-clear-button
                   @change="checkDays">
      </time-picker>

    </template>
  </form>
</template>
<script setup>
import {ref} from "vue";
import jsLogger from 'js-logger';

const log = jsLogger.get('ClassModelEditor');

const days = ['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche']

const dayRows = [
  {
    day: ref(),
    time: ref()
  }
]

function checkDays() {
  log.debug("Dates: " + JSON.stringify(dayRows.map(row => row.day.value + ', ' + JSON.stringify(row.time.value))));
  let lastRow = dayRows.slice(-1)[0];
  let day = lastRow.day.value;
  if (!day || day.length === 0) {
    return;
  }
  let time = lastRow.time.value;
  if (!time || time.HH.length === 0 || time.mm.length === 0) {
    return;
  }
  dayRows.push({
    day: ref(),
    time: ref()
  })
}
</script>

The error occurs when I try to push a new row into the dayRows array, in the checkDays function:

runtime-core.esm-bundler.js?d2dd:38 [Vue warn]: Unhandled error during execution of scheduler flush. This is likely a Vue internals bug. Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/core 
  at <ClassModelEditor> 
  at <ClassView onVnodeUnmounted=fn<onVnodeUnmounted> ref=Ref< Proxy {__v_skip: true} > > 
  at <RouterView> 
  at <App>

Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'el')
    at patchBlockChildren (runtime-core.esm-bundler.js?d2dd:5342:1)
    at processFragment (runtime-core.esm-bundler.js?d2dd:5418:1)
    at patch (runtime-core.esm-bundler.js?d2dd:5031:1)
    at patchKeyedChildren (runtime-core.esm-bundler.js?d2dd:5814:1)
    at patchChildren (runtime-core.esm-bundler.js?d2dd:5757:1)
    at patchElement (runtime-core.esm-bundler.js?d2dd:5270:1)
    at processElement (runtime-core.esm-bundler.js?d2dd:5118:1)
    at patch (runtime-core.esm-bundler.js?d2dd:5035:1)
    at patchKeyedChildren (runtime-core.esm-bundler.js?d2dd:5814:1)
    at patchChildren (runtime-core.esm-bundler.js?d2dd:5757:1)
patchBlockChildren @ runtime-core.esm-bundler.js?d2dd:5342
processFragment @ runtime-core.esm-bundler.js?d2dd:5418
patch @ runtime-core.esm-bundler.js?d2dd:5031
patchKeyedChildren @ runtime-core.esm-bundler.js?d2dd:5814
patchChildren @ runtime-core.esm-bundler.js?d2dd:5757
patchElement @ runtime-core.esm-bundler.js?d2dd:5270
processElement @ runtime-core.esm-bundler.js?d2dd:5118
patch @ runtime-core.esm-bundler.js?d2dd:5035
patchKeyedChildren @ runtime-core.esm-bundler.js?d2dd:5814
patchChildren @ runtime-core.esm-bundler.js?d2dd:5757
processFragment @ runtime-core.esm-bundler.js?d2dd:5437
patch @ runtime-core.esm-bundler.js?d2dd:5031
componentUpdateFn @ runtime-core.esm-bundler.js?d2dd:5660
run @ reactivity.esm-bundler.js?89dc:185
instance.update @ runtime-core.esm-bundler.js?d2dd:5694
callWithErrorHandling @ runtime-core.esm-bundler.js?d2dd:155
flushJobs @ runtime-core.esm-bundler.js?d2dd:396
Promise.then (async)
queueFlush @ runtime-core.esm-bundler.js?d2dd:285
queueJob @ runtime-core.esm-bundler.js?d2dd:279
eval @ runtime-core.esm-bundler.js?d2dd:5692
triggerEffect @ reactivity.esm-bundler.js?89dc:394
triggerEffects @ reactivity.esm-bundler.js?89dc:384
triggerRefValue @ reactivity.esm-bundler.js?89dc:1000
eval @ reactivity.esm-bundler.js?89dc:1131
triggerEffect @ reactivity.esm-bundler.js?89dc:394
triggerEffects @ reactivity.esm-bundler.js?89dc:379
triggerRefValue @ reactivity.esm-bundler.js?89dc:1000
eval @ reactivity.esm-bundler.js?89dc:1131
triggerEffect @ reactivity.esm-bundler.js?89dc:394
triggerEffects @ reactivity.esm-bundler.js?89dc:379
triggerRefValue @ reactivity.esm-bundler.js?89dc:1000
eval @ reactivity.esm-bundler.js?89dc:1131
triggerEffect @ reactivity.esm-bundler.js?89dc:394
triggerEffects @ reactivity.esm-bundler.js?89dc:379
triggerRefValue @ reactivity.esm-bundler.js?89dc:1000
eval @ reactivity.esm-bundler.js?89dc:1131
triggerEffect @ reactivity.esm-bundler.js?89dc:394
triggerEffects @ reactivity.esm-bundler.js?89dc:379
trigger @ reactivity.esm-bundler.js?89dc:352
set @ reactivity.esm-bundler.js?89dc:523
set @ runtime-core.esm-bundler.js?d2dd:3158
select @ VueTimepicker.vue?d39b:1193
onClick @ VueTimepicker.vue?d39b:30
callWithErrorHandling @ runtime-core.esm-bundler.js?d2dd:155
callWithAsyncErrorHandling @ runtime-core.esm-bundler.js?d2dd:164
invoker @ runtime-dom.esm-bundler.js?2725:369

There are a few things I am really really not sure about:

  1. I want the javascript to update the dayRows variable, which is used as the v-for target: should that variable be a reactive object for that to work?

  2. I set up dayRows to be an array of objects containing reactive properties. Should I rather make the entire array reactive? E.g.

const dayRows = reactive([ { day: '', time: {} } ])

If I do that what do I need to change in the template (v-for & v-model attributes in particular…)

  1. I don’t understand why v-model="dayRow.day.value" works (found it on the forums), even though when using v-model outside of v-for I would simply bind to the ref (v-model=“dayRef”, not dayRef.value)

Any suggestion will be greatly appreciated!

I think I found the answer to one of my questions in the docs:

"When refs are accessed as top-level properties in the template, they are automatically “unwrapped”
… which brings another (philosophical) question though: if the template only ever sees the .value of a ref, how does the reactive magic happen?

Meaning that with
const myRef = ref(2);
if
<input v-model="myRef"/>
really get unwrapped as
<input v-model="myRef.value"/>
then the template engine sees v-model=“2”? Or is Vue parsing the myRef.value expression and getting ahold of myRef?

One more semi-random attempt seems to have achieved what I was after:

const dayRows = [
  {
    day: ref(),
    time: ref()
  }
]
const getDayRows = computed(() => dayRows);
<template v-for="(dayRow, dayIndex) in getDayRows" :key="dayIndex">

This is slowly beginning to make sense :slight_smile:

In case anyone ends up here after a search… here is a simpler example that works using a reactive array. Using a computed as above won’t support adding or deleting rows on the fly.

The issue IMHO is with the documentation, which isn’t all that clear on the topic of automatic unwrapping of refs. In the example below everything works without having to use any explicit .value, but I sure can’t understand why (non top-level property access “row.time.HH” in the template, and direct access in the handlers).

<template>
  <div>
    <template v-for="(row, index) in rows" :key="index">
      <div class="container d-flex">
        <input :name="'city' + index" :id="'city' + index"
               v-model="row.city"
               @change="checkRows"/>
        <input :name="'hh' + index" :id="'hh' + index"
               v-model="row.time.HH"
               @change="checkRows"/>
        <input :name="'mm' + index" :id="'mm' + index"
               v-model="row.time.mm"
               @change="checkRows"/>
      </div>
    </template>
    <div>
      {{ JSON.stringify(rows) }}
    </div>
    <button @click="incrementTime">Increment</button>
  </div>
</template>
<script setup>
import {reactive, ref} from "vue";

const rows = reactive([
  {
    city: ref(''),
    time: ref({HH: '', mm: ''})
  }
])

function checkRows() {
  const lastRow = rows[rows.length - 1];
  if (lastRow.city.length > 0 && lastRow.time.HH.length > 0 && lastRow.time.mm.length > 0) {
    rows.push({
      city: ref(''),
      time: ref({HH: '', mm: ''})
    })
  }
}

function incrementTime() {
  rows.map(rows => {
    if (rows.time.HH.length > 0) {
      rows.time.HH = '33'
    }
  })
}

</script>

In Vue, state is deeply reactive by default.
When using reactive(), you don’t need to use ref() on the property again.
Array by reactive() will automatically handle new members.
Reactivity Fundamentals | Vue.js (vuejs.org)

Yes, I saw that, but also: “ It also means that when we assign or destructure a reactive object’s property into local variables, or when we pass that property into a function, we will lose the reactivity connection:”

I had been assuming that when using the reactive array in a v-for loop each iteration would “assign to a local variable” and thus lose reactivity?