Jeff
Jeff

Reputation: 309

Vuex update array as value of object

I'm running into a problem updating data in the Vuex store within a slightly nested data structure.

My full project is a bit more complex than what is below, but the issue I'm running into now is simplified and set up so that for each project page there will be a landing page that houses the table visibility, as well as the individual table component, within which there is a Vuetify v-data-table element that simply takes in the headers and items as props and displays them. It can be visualized in the following form:

- Project Page
  |- Landing page
     |- Table Visibility
     |- Table Component
        |- v-data-table

One way to think about it is as follows: For each animal (project page), there are three different breeds/types, each with their different characteristics. Thus, the above-mentioned structure would become:

- Dog
  |- Landing page
     |- Table Visibility
     |- German Shepherd
        |- v-data-table
     |- Bull Terrier
        |- v-data-table
     |- Labrador Retriever
        |- v-data-table
- Cat
  |- Landing page
     |- Table Visibility
     |- Russian Blue
        |- v-data-table
     |- British Shorthair
        |- v-data-table
     |- Persian
        |- v-data-table
- Bird
  |- Landing page
     |- Table Visibility
     |- Cockatiel
        |- v-data-table
     |- Parrot
        |- v-data-table
     |- Macaw
        |- v-data-table

When the user navigates to any of the project pages, then he or she will have the ability to choose, via the table visibility component, which tables will be view-able. This component looks something like this:

TableVisibility.vue ### -> tableTitles now pulled in as a prop from parent component

<template>
  <div>
    <v-card>
      <v-card-title>
        <p class="title ma-0">Table Visibility</p>
      </v-card-title>
      <v-divider class="mx-5"></v-divider>
      <v-card-text>
        <v-layout row wrap fill-height>
          <v-checkbox
            v-for="(title, idx) in tableTitles"
            v-model="tableVisibility"
            :label="title"
            :value="title"
            :key="idx"
            class="mx-1"
            multiple
          ></v-checkbox>
        </v-layout>
      </v-card-text>
      <v-card-actions>
        <v-switch
          v-model="showAll"
          :label="showAll ? 'Hide All' : 'Show All'"
        ></v-switch>
        <v-spacer></v-spacer>
      </v-card-actions>
    </v-card>
  </div>
</template>

<script>
  import { mapState } from 'vuex'
  export default {
    name: "TableChoices",
    props: ['tableTitles'],
    data() {
      return {
        showAll: false,
        displayTables: [],
      }
    },
    methods: {
    },
    computed: {
      ...mapState({
        pageName: state => state.pageName,
      }),
    },
    watch: {
      showAll(bool) {
        bool ?
          this.displayTables = this.tableTitles :
          this.displayTables = []
      },
      displayTables: {
        handler() {
          let tableObj = {};
          this.tableTitles.forEach(title => { tableObj[title] = this.displayTables.indexOf(title) > -1 })
          this.$store.commit('setTableVisibility', {page: this.pageName, tables: tableObj})
          if (this.displayTables.length === this.tableTitles.length) {
            this.showAll = true
          } else if (this.displayTables.length === 0) {
            this.showAll = false
          }
        }
      },
    }
  }
</script>

<style scoped>

</style>

LandingPage.vue

<template>
  <div>
  </div>
</template>

<script>
  import Dog from '@/components/Dog'
  import Cat from '@/components/Cat'
  import Bird from '@/components/Bird'
  import { mapState, mapGetters } from 'vuex'
  export default {
    name: "LandingPage",
    components: {
      Dog,
      Cat,
      Bird,
    },
    data() {
      return {
        items: {},
        headers: {},
      }
    },
    computed: {
      ...mapState({
        pageName: state => state.pageName,
      }),
      ...mapGetters({
        tableVisibility: 'getTableVisibility'
      })
    },
    watch: {
      tableVisibility: {
        handler() { console.log('tableVisibility in LandingPage.vue', this.tableVisibility)},
        deep: true
      },
    }
  }
</script>

<style scoped>

</style>

There's no point in putting up the Dog, Cat, or Bird component because the only thing they hold is the Vuetify data table so they could essentially simply by placeholders as they're not important.

The store is set up like this:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)
export const debug = process.env.NODE_ENV !== 'production'

function initialState() {
  return {
    pageItems: {},
    pageHeaders: {},
    pageName: '',
    tableTitles: {
      Dog: ['German Shepherd', 'Bull Terrier', 'Labrador Retriever'],
      Cat: ['Russian Blue', 'British Shorthair', 'Persian'],
      Bird: ['Cockatiel', 'Parrot', 'Macaw']
    },
    tableVisibility: {
      Dog: {
        German Shepherd: false, 
        Bull Terrier: false, 
        Labrador Retriever: false
      },
      Cat: {
        Russian Blue: false,
        British Shorthair: false,
        Persian: false
      },
      Bird: {
        Cockatiel: false, 
        Parrot: false,
        Macaw: false
      }
    }
  }
}

const state = {...initialState()}

const mutations = {
  setTableVisbility(state, payload) {
    const page = payload.page;
    const tables = payload.tables;
    // Vue.set(state.tableVisibility, page, tables)
    state.tableVisibility[page] = Object.assign({}, tables);
  }
}

const getters = {
  getTableVisibility: (state) => ( state.tableVisibility[state.PageName] ),
  getCurrentPageTableTitles: (state) => ( state.tableTitles[state.pageName] ),
}
export default new Vuex.Store({state, mutations, getters, strict: debug})

Like I said, the total project is more complicated than this (the store functionality I'm displaying here is actually within a module, etc) but I'm having trouble even getting the updated value of tableVisibility in the LandingPage.vue component. I'm trying a couple of different methods of applying the deep handler to the watcher (https://v2.vuejs.org/v2/api/#watch and https://medium.com/@stijlbreuk/six-random-issues-and-their-solutions-in-vuejs-b16d470a6462); I've tried splitting out the array functionality in the store by clearing the array using splice (How do I empty an array in JavaScript?) and then pushing all elements back into the array; I've tried using the filter array method to try and create a new array; I've tried storing tableVisibility as an object (with the table name as the key and the value as true or false) so that I can make use of Object.assign (https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats); I've found this page regarding normalizing data (https://forum.vuejs.org/t/vuex-best-practices-for-complex-objects/10143/2), but it seems like it's not terribly complex to the point where it's already kind of simple.

I'm at the end of my wits trying to figure out what's going on and I'd really appreciate help.

Edit

I've updated the project to include getters, and reactivity is working in all other components except the tableVisibility value. I've tried leaving the tableVisibility elements as arrays, then I tried converting them to objects, as is seen in this edit, and I tried using Vue.set to update the values, which did not work, then I tried using Object.assign as is shown in this current version. If I navigate away from a page and then back the values are updated, it's just that they seem to be losing reactivity despite my trying to use Vue.set and Object.assign.

Upvotes: 0

Views: 3258

Answers (1)

blaz
blaz

Reputation: 4068

My suggestion for you is to start thinking about normalizing your data. Some big issues are:

  • Page and Table names are totally hard-code as string in your state. It causes trouble if you need to expand the number of pages/tables. Same goes for tableVisibility.
  • Your state is deeply nested while you don't have any getter methods, which force you to expose the state structure to your Vue component, which make updating Vuex store very difficult.

First, write getter methods and mutation methods; make them generic enough so that you can reuse anywhere in Vue components, and force all your Vue components use them instead of directly accessing state

Example:

const getters = {
    getCurrentActivePage: (state) => {}, // return pageName
    getProjects: (state) => {},
    getProjectById: (state) => (projectId) => {},
    getTablesOfProjects: (state) => (projectId) => {},
    getTable: (state) => (tableID) => {},
    isTableVisible: (state) => (tableID) => {}
}

const mutations = {
    setTableVisibility: (state, { tableID, newValue }) => {}
}

After decoupling UI logic from state structure, you can start normalizing your state data. Try to see your state as a small database, and design it logically. One way to do it is like this:

const state = {
    pageName: '',
    projects: {
        '123': { id: '123', name: 'Dog' },
        '234': { id: '234', name: 'Cat' }
        // ...
    },
    tables: {
        '123z': { id: '123z', projectId: '123', name: 'German Shepherd', visible: false }
    }
}

where id can be auto-generate using npm packages like nanoid.

Even without normalization, I still recommend you to do step 1 (decoupling). If your store is complex, you can never be certain if modifying store's state will cause Vue components to break down. Good getters and mutations will at least catch invalid data and return default value for your component.

Upvotes: 2

Related Questions