3

I'm dealing with a problem I can't figure out where I have an array of items that should be rendered in a component but inside that component they can be manipulated into a new array, so whenever a change is made into one of the items it should be pushed into the itemsToEdit array, instead of modifying the original item because later I need to send that new array to the server with only items modified and only the fields modified...

My child component has a simple checkbox (that is working the way it should) with a checked property which shows the default value if given, and a v-model with all the logic that actually works.

If I set up the v-model to v-model="item.show" it changes the original item, so there's nothing to change in there, but I can't send from parent to children the itemsToEdit array because it is empty and v-model="items.id.show" won't work.

I've worked with multiple checkboxes and an array v-model but it is a different workflow because I actually edit the original array of items, so it will push/remove items as I check the checkboxes but that's not what I want, the original array should stay as it is all the time.

Here's my simplified code, the children actually has a lot of checkboxes but I'll show just one because simplicity.

Parent component

<template>
    <div>
        <TestComponent v-for="i in items" :key="i.id" :item="i" :items-to-edit="itemsToEdit"/>
    </div>
</template>

<script>
import TestComponent from '@/TestComponent'
export default {
    name: 'MyParent',
    components: { TestComponent },
    data () {
        return {
            items: [
                { id: 1, name: 'test', show: false },
                { id: 2, name: 'test 2', show: false },
                { id: 3, name: 'test 3', show: true },
                { id: 4, name: 'test 4', show: false }
            ],
            itemsToEdit: []
        }
    }
}
</script>

Child component

<template>
    <tr>
        <td>{{ item.id }}</td>
        <td>{{ item.name }}</td>
        <td>
            <MyCheckbox :checked="item.show"/>
        </td>
    </tr>
</template>

<script>
export default {
    name: 'TestComponent',
    components: { MyCheckbox },
    props: ['item', 'itemsToEdit']
}
</script>

EDIT: One thing I forgot, I obviously can use $emit and listen on parent then push into the array, but that's not what I want, I am looking for a better way to implement this, if I have no other option, I will go with the events.

EDIT2: I can't 'clone' original array into the itemsToEdit because I want that array to be only filled up whenever a real change comes in, because later, the request send to server will only contain real changes, if I send the whole array of id's it will try to modify them even if they have no changes so it will be a waste of performance checking everything serverside.

7
  • You can just add another array to your data, say, currentItems, and make its initial value a copy of the original array. Then, on your code, mutate currentItems instead of items Commented Oct 25, 2019 at 12:49
  • I've updated my question because I forgot to mention that I don't want to do what you say, because that'll be like using the original array which is not what I want, I wonly want the real changes to be pushed, if some item hasn't changed, it shouldn't be on the edit array ^^ Commented Oct 25, 2019 at 12:54
  • You didn't get it, I meant using three arrays (items, currentItems, and itemsToEdit) instead of the two you currently use Commented Oct 25, 2019 at 12:55
  • No judgement, but why are you trying to work around using the event/$emit approach? Commented Oct 25, 2019 at 12:56
  • @BDawg because I'm looking towards another way to handle it, I know I can do it with events but if I can do it in a way I don't even need to listen for stuff, that's better, because we don't have buttons (I wish... that'll simplify all), everything should be fired through checks, hence the v-model approach (which of course may or not may be the best solution) I'm just curious about how other people would approach this without $emits. @Ayrton oh yeah, didn't get it at all, but it will have the same effect as using the original one, right? I'll have items without changes inside that array? Commented Oct 25, 2019 at 12:59

1 Answer 1

1

Regardless of your selected approach, my recommendation is to keep your list logic in one place (if you can) for the sake of easier maintenance. Side effects are necessary at times, but they can be very difficult to work with as they spread out.

Also, I'm not sure if this is the problem you are running into, but I can see why $emit might be a problem if you made a different event for each of your checkboxes/changed values. I think you can consolidate this into a single event like item-updated to prevent things from getting too unwieldy. For example, in the snippet below, I've used a shared method named updateItem in the input event listeners on each checkbox like this: @change="updateItem({ show: $event.target.checked })" and @change="updateItem({ active: $event.target.checked })". This way, there is just one $emit call (inside the updateItem method). Try running the snippet below - I think it should give you the results you were looking for:

Vue.config.devtools=false
Vue.config.productionTip = false

// Parent element
Vue.component('v-items', {
  template: `
    <div class="items">
      <div class="items__list">
        <v-item v-for="i in items" :key="i.id" :item="i" @item-updated="itemUpdated"/>
      </div>
    </div>
  `,
  data () {
    const origItems = [
      { id: 1, name: 'test', show: false, active: true },
      { id: 2, name: 'test 2', show: false, active: true },
      { id: 3, name: 'test 3', show: true, active: true },
      { id: 4, name: 'test 4', show: false, active: true },
    ]
    return {
      origItems,
      items: origItems.map(item => Object.assign({}, item)),
      editedItems: [],
    }
  },
  methods: {
    itemUpdated(item) {
      const origItem = this.origItems.find(o => o.id === item.id)
      const indexInEdited = this.editedItems.findIndex(o => o.id === item.id)
      const objectChanged = JSON.stringify(item) !== JSON.stringify(origItem)
      
      if (indexInEdited !== -1) {
        this.editedItems.splice(indexInEdited, 1)
      }

      if (objectChanged) {
        this.editedItems.push(item)
      }
      
      // Show the editedItems list
      console.clear()
      console.log(this.editedItems)
    }
  },
})

// Child elements
Vue.component('v-item', {
  props: ['item'],
  template: `
    <div class="item">
      <div>{{ item.id }}</div>
      <div>{{ item.name }}</div>
      <input type="checkbox"
        :checked="item.show"
        @change="updateItem({ show: $event.target.checked })"
      />
      <input type="checkbox"
        :checked="item.active"
        @change="updateItem({ active: $event.target.checked })"
      />
    </div>
  `,
  methods: {
    updateItem(update) {
      // Avoid directly modifying this.item by creating a cloned object
      this.$emit('item-updated', Object.assign({}, this.item, update))
    }
  },
})

new Vue({ el: "#app" })

console.clear()
body {
  background: #20262E;
  padding: 20px;
  font-family: Helvetica;
  min-height: 300px;
}

#app {
  background: #fff;
  border-radius: 4px;
  padding: 20px;
}

.items {
  display: grid;
  grid-auto-flow: row;
  gap: 10px;
}

.items__list {
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  gap: 5px;
}

.item {
  display: contents;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <v-items></v-items>
</div>

Sign up to request clarification or add additional context in comments.

2 Comments

Yeah! You got the point about the checkboxes :D Thanks for your code, it helped me to see thinks from another perspective and ended up with something working ^^
Glad it helped!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.