Search code examples
vue.jsvue-componentvuex

Proper way to use Vuex with child components


I have used VueJS and Vuex before. Not in production, just for some simple, personal side projects and it felt pretty straight forward.

However, now I face a problem that is just slightly more complex but using Vuex already feels way more complicated. So I'm looking for some guidance here.

In the main view I present a list of cards to the user. A card is an editable form of about ten properties (inputs, selects, etc.). I obviously implemented a component for these cards, since they are repeated in a list view. My first naive approach was to fetch a list of - let's call it forms - from my store and then using v-for to present a card for each form in that list of forms. The form is passed to the child component as a property.

Now I want to bind my form controls to the properties. To do this I implemented Two-way Computed Properties to properly utilize my store mutations. But it feels extremely repetitive to implement a custom computed property with getter and setter plus mutation for every property on my model. Additionally, the forms are an array in my state. So I have to pass the id of the form to edit in the store to every mutation.

Something else I had in mind was just passing the store id of the form to the child component and have "by id" getters for every property of my model as well as a matching mutation. But this doesn't feel like the proper way to do this, either. It is essentially the same, right?!

Is there a better solution to this problem? Maybe I'm just missing something or I'm overcomplicating things.

A trimmed down example:

Editor.vue:

<template>
  <v-container>
    <EditableCard v-for="(card, i) in cards" :key="i" :card="card" />
  </v-container>
</template>

<script>
import EditableCard from "@/components/EditableCard";

import { mapGetters } from "vuex";

export default {
  name: "Editor",

  components: {
    EditableCard
  },

  computed: {
    ...mapGetters("cards", {
      cards: "list"
    })
  }
};
</script>

EditableCard:

<template>
  <v-card>
    <v-form>
      <v-card-title>
        <v-text-field v-model="title"></v-text-field>
      </v-card-title>

      <v-card-text>
        <v-text-fieldv-model="text"></v-text-field>
        <!-- And some more fields... -->
      </v-card-text>
    </v-form>
  </v-card>
</template>

<script>
import { mapMutations } from "vuex";

export default {
  name: "EditableCard",

  props: {
    card: Object
  },

  computed: {
    title: {
      get() {
        return card.title;
      },
      set(value) {
        this.setCardTitle(this.card.id, value);
      }
    },

    text: {
      get() {
        return card.text;
      },
      set(value) {
        this.setCardText(this.card.id, value);
      }
    }

    // Repeat for every form input control
  },

  methods: {
    ...mapMutations("cards", {
      setCardTitle: "setTitle",
      setCardText: "setText"

      // Repeat for every form input control
    })
  }
};
</script>

Solution

  • It would be nice to create a computed setter for the whole form object using a clone, but this won't work because changes won't trigger the computed setter.

    If anyone wants to explore this interesting failure, see here)

    To work around this, you can use a watch and a data clone:

    <v-form>
      <v-text-field v-model="clone.title" />
      <v-text-field v-model="clone.text" />
    </v-form>
    
    props: ['index', 'card'],
    data() {
      return {
        clone: {}
      }
    },
    watch: {
      card: {
        handler(card) {
          this.clone = { ...card }
        },
        immediate: true,
        deep: true
      },
      clone: {
        handler(n,o) {
          if (n === o) {
            this.$store.commit('SET_CARD', { index: this.index, card: n })
          }
        },
        deep: true
      }
    }
    

    Your v-for:

    <EditableCard v-for="(card, index) in cards" :card="card" :index="index" :key="index" />
    

    The mutation:

    mutations: {
      SET_CARD(state, { index, card }) {
        Vue.set(state.cards, index, card);
      }
    }
    

    This is way more complex than it should need to be... but it works.