Search code examples
javascriptvue.jsvuejs2vuexcomputed-properties

Editing object in array returned from computed property


In my Vuex store I get an array of rooms from a Firebase/Firestore API call, which looks like this: [{label:'Kitchen', decription:'...'}, ...], and I make it available to my app via a Vuex getter function. Naturally in my components I then receive this array as a computed property. But in my CRUD component I actually route this "original" array through another computed property so that I can attach several temporary properties to each object in the array - workable: () => this.rooms.map(val => { return ...val, editing: false, loading:false, ... }). These additional properties are exclusively needed for the CRUD component and won't make sense to keep on the original array, the data source, or anywhere else in the app for that matter.

The problem that I'm now having is I obviously need to be able to update these temporary properties in the CRUD array, but I can't, because (AFAIU) my computed properties would need setters that would update the original array in the vuex store, which in my case would then have to update the data source, to complete the loop. (Note that I don't fully understand most of what I said in this paragraph so feel free to correct any wrong assumptions.)

So my question is how do I update the temporary properties in my array's objects without these changes (and properties) propogating all the way back to the data source?

Here's a simplified version of my code to help you visualise how things currently work:

// store.js (Vuex)
const actions = {
  load: ({ commit, getters }) => {
    const query = firebase.roomCollection.where('user', '==', getters.user.id);
    const unsub = query.onSnapshot(snap => {

      const rooms = snap.docs.map(val => {
        return {
          id: val.id,
          ...val.data(),
          timestamp: val.data().timestamp.toDate(),
        };
      });

      commit('setRooms', rooms);
    });
  },
};

const mutations = {
  setRooms: (state, payload) => state.rooms = payload,
};

const state = {
  rooms: [],
};

const getters = {
  rooms: state => state.rooms,
};

// RoomCrud.vue
<template>
  <ul>
    <li v-for="room in workable" :key="`room-${i}`">
      {{ room.label }}
      <button @click="room.editing = !room.editing">Edit</button>
      <br />
      room: {{ room }} <!-- room.editing doesn't change when the Edit button gets clicked -->
    </li>
  </ul>
</template>

<script>
  import { mapGetters } from 'vuex';

  export default {
    computed: {
      ...mapGetters(['rooms']),
      workable() {
        return this.rooms.map(val => {
          return {
            ...val,
            editing: false,
            loading: false
          };
        });
      }
    }
  }
</script>

I have tried making workable an (initially null) data property and then using a method (setWorkable()) to add the additional properties to each array, and then calling setWorkable() on mounted(), which successfully detatches workable from rooms, but the problem with this approach is that I keep having to call setWorkable() when I make a change to one of the objects in the array that I do want to have propagate all the way back to the source.

I realise that this might be the only way to do what I'm trying to do, but I'm hoping there's a better way that only uses computed properties, so that I don't have to manually rerender every time a change is made.

Let me know if I need to include any other information.


Solution

  • I suggest to change the approach to put the whole logic to the store by adding an action edit which will be triggered when you click on that button by passing the room index and changing the row state :

    // store.js (Vuex)
    const actions = {
     edit:({commit,state},index)=>{
         commit('editRoom', index);
      },
      load: ({ commit, getters }) => {
        const query = firebase.roomCollection.where('user', '==', getters.user.id);
        const unsub = query.onSnapshot(snap => {
    
          const rooms = snap.docs.map(val => {
            return {
              id: val.id,
              ...val.data(),
                editing: false,
                loading: false,
              timestamp: val.data().timestamp.toDate(),
            };
          });
    
          commit('setRooms', rooms);
        });
      },
    };
    
    const mutations = {
      setRooms: (state, payload) => state.rooms = payload,
       editRoom:(state,index)=> {
              state.rooms[index].editing=!state.rooms[index].editing;
               Vue.set(state.rooms,index,state.rooms);
    
    
           }
    };
    
    const state = {
      rooms: [],
    };
    
    const getters = {
      rooms: state => state.rooms,
    };
    

    RoomCrud.vue

    <template>
      <ul>
        <li v-for="(room,i) in rooms" :key="`room-${i}`">
          {{ room.label }}
          <button @click="edit({index:i})">Edit</button>
          <br />
          room: {{ room }}
        </li>
      </ul>
    </template>
    
    <script>
      import { mapGetters,mapActions } from 'vuex';
    
      export default {
        methods:{
                ...mapActions(["edit"]),
            } ,
        computed: {
          ...mapGetters(['rooms']),
    
        }
      }
    </script>