skuallpa
skuallpa

Reputation: 1287

Vue 3 v-model binding not working when passing filtered data

I have a situation where a change in some data which is filtered before being passed to a child component using the v-model binding is not detected. I'm using the multiple v-model binding syntax coming from Vue 3 (v-model:myObject="myObject").

Here is a minimalistic example that reproduce the issue.

This is a child component to edit the a user (firstname and lastname). I'm passing an object (the user) as a prop.

// User.vue (Child component)
<script>
import _ from 'lodash'

export default {
  props: {
    user: Object
  },
  emits: [
    'update:user'
  ],
  methods: {
    updateUser() {
      this.$emit('update:user', this.userEdit)
    }
  },
  data() {
    return {
      userEdit: _.cloneDeep(this.user)
    }
  }
}
</script>

<template>
  <div>
    <input
      type="text"
      v-model="userEdit.firstName"
    />
    <input
      type="text"
      v-model="userEdit.lastName"
    />
    <button @click="updateUser">
      Update
    </button>
  </div>
</template>

And here below is the parent component/page that uses this component. I want to edit all the users with firstName starting with 'jean', so I'm using a computed property to filter the users (filteredUsers).

I have a watcher to detect any changes in the users array.

My problem is that when I'm filtering the users (in the filteredUsers computed property), then the users array is no longer getting updated in the parent page (when making changes in the child component). If I simply return the users (without filtering) in the computed property, then the users array is well updated.

// Parent component
<template>
  <div>
    <User 
      v-for="(user, index) in filteredUsers"
      :key="index"
      v-model:user="filteredUsers[index]"
    ></User>
  </div>
</template>

<script>
import User from '../components/User.vue'

export default {

  components: {
    User
  },
  data() {
    return {
      users: [
        {
          firstName: 'jean',
          lastName: 'paul',
        },
        {
          firstName: 'pierre',
          lastName: 'smith',
        },
        {
          firstName: 'jean',
          lastName: 'valjean',
        }
      ],
    }
  },

  computed: {
    filteredUsers() {
      // Problem: When filtering the users, users is no longer getting updated when editing the user in the child component.
      return this.users.filter(user => user.firstName.includes('jean'))
      
      // If not filtering, then users is well updated and the watcher notice the change.
      // return this.users
    }
  },

  watch:{
    users: {
      handler(newVal, oldVal) {
        console.log('users changed')
      },
      deep: true
    }
  }
}

</script>

I guess this has to do with the fact that the filter method does a shallow copy.

Any idea how to make it working?

Upvotes: 0

Views: 1039

Answers (2)

ieatbytes
ieatbytes

Reputation: 516

You are processing all the data based on a computed property which is producing the results based on users array as a "new object". You have to find the array index and then update it. Review my code below: I have only changed the Parent component, integrated the event listener and a function that processes it. Happy Coding!

// Parent component
<template>
  <div>
    <User v-for="(user, index) in filteredUsers" @update:user="updateUser" :key="index"
      v-model:user="filteredUsers[index]"></User>
  </div>
</template>

<script>
import _ from 'lodash'
import User from './components/User.vue'

export default {

  components: {
    User
  },
  data() {
    return {
      users: [
        {
          id: 1, //we are using it to identify user data
          firstName: 'jean',
          lastName: 'paul',
        },
        {
          id: 2,
          firstName: 'pierre',
          lastName: 'smith',
        },
        {
          id: 3,
          firstName: 'jean',
          lastName: 'valjean',
        }
      ],
    }
  },

  computed: {
    filteredUsers() {
      // Problem: When filtering the users, users is no longer getting updated when editing the user in the child component.
      return this.users.filter(user => user.firstName.includes('jean'))

      // If not filtering, then users is well updated and the watcher notice the change.
      // return this.users
    }
  },
  methods: {
    updateUser(user) {

      let uIdx = _.findIndex(this.users, function (u) { return u.id == user.id; });
      console.log('user', user, uIdx);
      if (this.users[uIdx]) {
        this.users[uIdx] = user;
      }

    }
  },
  watch: {
    users: {
      handler(newVal, oldVal) {
        console.log('users changed')
      },
      deep: true
    }
  }
}

</script>

Another method which can work without "$emit"

//Parent Component
<template>
  <div>
    {{ users }}
    <hr>{{ filteredUsers }}
    <User v-for="(user, index) in filteredUsers" :key="index" v-model:user="filteredUsers[index]"></User>
  </div>
</template>

<script>
import User from './components/User.vue'

export default {

  components: {
    User
  },
  data() {
    return {
      users: [
        {
          firstName: 'jean',
          lastName: 'paul',
        },
        {
          firstName: 'pierre',
          lastName: 'smith',
        },
        {
          firstName: 'jean',
          lastName: 'valjean',
        }
      ],
    }
  },

  computed: {
    filteredUsers() {
      return this.users.filter(user => user.firstName.includes('jean'))
    }
  },

  watch: {
    users: {
      handler(newVal, oldVal) {
        console.log('users changed')
      },
      deep: true
    }
  }
}

</script>

We are not using emit in child component

// Child User.vue
<script>

export default {
    props: {

        user: Object
    }

}
</script>

<template>
    <div>

        <input type="text" v-model="user.firstName" />
        <input type="text" v-model="user.lastName" />
    </div>
</template>

Upvotes: 0

skuallpa
skuallpa

Reputation: 1287

OK. Adding the event listener @update:user and handling the update manually (using a custom method) solves the issue.

But if I bind directly to the properties of the user object, as bellow, then the users array (in the parent) gets updated properly, even though I'm using the filteredUsers property.

<template>
  <div>
    <User 
      v-for="(user, index) in filteredUsers"
      :key="index"
      v-model:first-name="filteredUsers[index].firstName"
      v-model:last-name="filteredUsers[index].lastName"
    ></User>
  </div>
</template>

<script>
import User from '../components/User.vue'

export default {

  components: {
    User
  },
  data() {
    return {
      users: [
        {
          firstName: 'jean',
          lastName: 'paul',
        },
        {
          firstName: 'pierre',
          lastName: 'smith',
        },
        {
          firstName: 'jean',
          lastName: 'valjean',
        }
      ],
    }
  },

  computed: {
    filteredUsers() {
      return this.users.filter(user => user.firstName.includes('jean'))
    }
  },

  watch:{
    users: {
      handler(newVal, oldVal) {
        console.log('users changed')
      },
      deep: true
    }
  }
}

</script>
// Child User.vue
<script>

export default {
  props: {
    firstName: String,
    lastName: String,
  },
  emits: [
    'update:firstName', 
    'update:lastName',
  ],
}
</script>

<template>
  <div>
    <input
      type="text"
      :value="firstName"
      @input="$emit('update:firstName', $event.target.value)"
    />
    <input
      type="text"
      :value="lastName"
      @input="$emit('update:lastName', $event.target.value)"
    />
  </div>
</template>

Why does it work in such case and not when passing the object as a prop (v-model:user="filteredUsers[index]") ? (as in my first question)

Upvotes: 1

Related Questions