Search code examples
vue.jsvuejs2vuex

Limiting the two way binding with vuejs / variable values change when modifying another variable


I have a 'client-display' component containing a list of clients that I get in my store via mapGetter. I use 'v-for' over the list to display all of them in vuetify 'v-expansion-panels', thus one client = one panel. In the header of those panels, I have a 'edit-delete' component with the client passed to as a prop. This 'edit-delete' basically just emits 'edit' or 'delete' events when clicked on the corresponding icon with the client for payload. When I click on the edit icon, the edit event is then catched in my 'client-display' so I can assign the client to a variable called 'client' (sorry I know it's confusing a bit). I pass this variable to my dialog as a prop and I use this dialog to edit the client.

So the probleme is : When I edit a client, it does edit properly, but if I click on 'cancel', I find no way to revert what happened in the UI. I tried keeping an object with the old values and reset it on a cancel event, but no matter what happens, even the reference values that I try to keep in the object change, and this is what is the most surprising to me. I tried many things for this, such as initiating a new object and assigning the values manually or using Object.assign(). I tried a lot of different ways to 'unbind' all of this, nothing worked out. I'd like to be able to wait for the changes to be commited in the store before it's visible in the UI, or to be able to have a reference object to reset the values on a 'cancel' event.

Here are the relevant parts of the code (I stripped a lot of stuff to try and make it easier to read, but I think everything needed is there):

Client module for my store

I think this part works fine because I get the clients properly, though maybe something is binded and it should not

const state = {
  clients: null,
};

const getters = {
  [types.CLIENTS] : state => {
    return state.clients;
  },
};

const mutations = {
  [types.MUTATE_LOAD]: (state, clients) => {
    state.clients = clients;
  },
};

const actions = {
  [types.FETCH]: ({commit}) => {
    clientsCollection.get()
      .then((querySnapshot) => {
        let clients = querySnapshot.docs.map(doc => doc.data());
        commit(types.MUTATE_LOAD, clients)
    }).catch((e) => {
      //...
    });
  },
}

export default {
  state,
  getters,
  mutations,
  ...
}

ClientsDisplay component

<template>
  <div>
    <div>
      <v-expansion-panels>
        <v-expansion-panel
          v-for="c in clientsDisplayed"
          :key="c.name"
        >
          <v-expansion-panel-header>
            <div>
              <h2>{{ c.name }}</h2>
              <edit-delete
                :element="c"
                @edit="handleEdit"
                @delete="handleDelete"
              />
            </div>
          </v-expansion-panel-header>
          <v-expansion-panel-content>
            //the client holder displays the client's info
            <client-holder
              :client="c"
            />
          </v-expansion-panel-content>
        </v-expansion-panel>
      </v-expansion-panels>
    </div>
    <client-add-dialog
      v-model="clientPopup"
      :client="client"
      @cancelEdit="handleCancel"
    />
  </div>
</template>

<script>
  import { mapGetters, mapActions } from 'vuex';
  import * as clientsTypes from '../../../../store/modules/clients/types';
  import ClientDialog from './ClientDialog';
  import EditDelete from '../../EditDelete';
  import ClientHolder from './ClientHolder';
  import icons from '../../../../constants/icons';

  export default {
    name: 'ClientsDisplay',
    components: {
      ClientHolder,
      ClientAddDialog,
      EditDelete,
    },
    data() {
      return {
        icons,
        clientPopup: false,
        selectedClient: null,
        client: null,
        vueInstance: this,
      }
    },
    created() {
      this.fetchClients();
    },
    methods: {
      ...mapGetters({
        'stateClients': clientsTypes.CLIENTS,
      }),
      ...mapActions({
        //this loads my clients in my state for the first time if needed
        'fetchClients': clientsTypes.FETCH,
      }),
      handleEdit(client) {
        this.client = client;
        this.clientPopup = true;
      },
      handleCancel(payload) {
        //payload.uneditedClient, as defined in the dialog, has been applied the changes
      },
    },
    computed: {
      isMobile,
      clientsDisplayed() {
        return this.stateClients();
      },
    }
  }
</script>

EditDelete component

<template>
  <div>
    <v-icon
      @click.stop="$emit('edit', element)"
    >edit</v-icon>
    <v-icon
      @click.stop="$emit('delete', element)"
    >delete</v-icon>
  </div>
</template>

<script>
  export default {
    name: 'EditDelete',
    props: ['element']
  }
</script>

ClientDialog component

Something to note here : the headerTitle stays the same, even though the client name changes.

<template>
  <v-dialog
    v-model="value"
  >
    <v-card>
      <v-card-title
        primary-title
      >
        {{ headerTitle }}
      </v-card-title>
      <v-form
        ref="form"
      >
        <v-text-field
          label="Client name"
          v-model="clientName"
        />
        <address-fields
          v-model="clientAddress"
        />
      </v-form>
      <v-card-actions>
        <v-btn
          @click="handleCancel"
          text
        >Annuler</v-btn>
        <v-btn
          text
          @click="submit"
        >Save</v-btn>
      </v-card-actions>
    </v-card>
  </v-dialog>
</template>

<script>
  import AddressFields from '../../AddressFields';

  export default {
    name: 'ClientDialog',
    props: ['value', 'client'],
    components: {
      AddressFields,
    },
    data() {
      return {
        colors,
        clientName: '',
        clientAddress: { province: 'QC', country: 'Canada' },
        clientNote: '',
        uneditedClient: {},
      }
    },
    methods: {
      closeDialog() {
        this.$emit('input', false);
      },
      handleCancel() {
        this.$emit('cancelEdit', { uneditedClient: this.uneditedClient, editedClient: this.client})
        this.closeDialog();
      },
    },
    computed: {
      headerTitle() {
        return this.client.name
      }
    },
    watch: {
      value: function(val) {
        // I watch there so I can reset the client whenever I open de dialog
        if(val) {
          // Here I try to keep an object with the value of this.client before I edit it
          // but it doesn't seem to work as I intend
          Object.assign(this.uneditedClient, this.client);
          this.clientName = this.client.name;
          this.clientContacts = this.client.contacts;
          this.clientAddress = this.client.address;
          this.clientNote = '';
        }
      }
    }
  }
</script>

Solution

  • To keep an independent copy of the data, you'll want to perform a deep copy of the object using something like klona. Using Object.assign is a shallow copy and doesn't protect against reference value changes.