thimplicity
thimplicity

Reputation: 87

VueJS DOM not updating in component

I have gone through the other "DOM not updating" posts, but have not been able to figure out a solution. I am trying to create a task app for starters and the app can already load from Firestore, add a new one and delete one. I have two problems, but I would like to start with the first one. When I add, delete data, the DOM is not updating dynamically, although the respective array in the Vuex store is updated correctly. When I reload the page, it is also updating.

I have added the code of the component and the store, I hope I am looking the in the right place, but I have the feeling I am not.

Thanks in advance

Store code:

import Vue from 'vue'
import Vuex from 'vuex'
// import firebase from 'firebase'
import router from '@/router'
import db from '@/db'
import firebase from '@/firebase'

Vue.use(Vuex)

export const store = new Vuex.Store({
  state: {
    appTitle: 'My Awesome App',
    addedTask: false,
    loadedTasks: false,
    deletedTasks: false,
    user: null,
    error: null,
    loading: false,
    tasks: [],
    task: {
      title: '',
      description: '',
      tags: [],
      dueTime: '',
      dueDate: '',
      reminderFlag: Boolean,
      quadrant: '',
      userId: '',
      timestamp: ''
    }
  },
  // Mutations (synchronous): change data in the store
  mutations: {
    setUser (state, payload) {
      state.user = payload
      state.task.userId = firebase.auth().currentUser.uid
    },
    setError (state, payload) {
      state.error = payload
    },
    setLoading (state, payload) {
      state.loading = payload
    },
    addedTask (state, payload) {
      state.addedTask = payload
    },
    loadedTasks (state, payload) {
      state.loadedTasks = true
    },
    deletedTask (state, payload) {
      state.deletedTask = payload
    }
  },
  // Actions (asynchronous/synchronous): change data in the store
  actions: {
    autoSignIn ({ commit }, payload) {
      commit('setUser', { email: payload.email })
    },
    userSignUp ({ commit }, payload) {
      commit('setLoading', true)
      firebase.auth().createUserWithEmailAndPassword(payload.email, payload.password)
        .then(firebaseUser => {
          commit('setUser', { email: firebaseUser.user.email })
          commit('setLoading', false)
          router.push('/home')
          commit('setError', null)
        })
        .catch(error => {
          commit('setError', error.message)
          commit('setLoading', false)
        })
    },
    userSignIn ({ commit }, payload) {
      commit('setLoading', true)
      firebase.auth().signInWithEmailAndPassword(payload.email, payload.password)
        .then(firebaseUser => {
          commit('setUser', { email: firebaseUser.user.email })
          commit('setLoading', false)
          commit('setError', null)
          router.push('/home')
        })
        .catch(error => {
          commit('setError', error.message)
          commit('setLoading', false)
        })
    },
    userSignOut ({ commit }) {
      firebase.auth().signOut()
      commit('setUser', null)
      router.push('/')
    },
    addNewTask ({ commit }, payload) {
      commit('addedTask', false)
      db.collection('tasks').add({
        title: payload.title,
        userId: firebase.auth().currentUser.uid,
        createdOn: firebase.firestore.FieldValue.serverTimestamp(),
        description: payload.description,
        dueDateAndTime: new Date(payload.dueDate & ' ' & payload.dueTime),
        reminderFlag: payload.reminderFlag,
        quadrant: payload.quadrant,
        tags: payload.tags
      })
        .then(() => {
          commit('addedTask', true)
          this.state.tasks.push(payload)
          commit('loadedTasks', true)
        })
        .catch(error => {
          commit('setError', error.message)
        })
    },
    getTasks ({ commit }) {
      commit('loadedTasks', false)
      db.collection('tasks').orderBy('createdOn', 'desc').get().then(querySnapshot => {
        querySnapshot.forEach(doc => {
          const data = {
            'taskId': doc.id,
            'title': doc.data().title,
            'quadrant': doc.data().quadrant
          }
          this.state.tasks.push(data)
        })
        commit('loadedTasks', true)
      })
    },
    deleteTask ({ commit }, payload) {
      db.collection('tasks').doc(payload).delete().then(() => {
        this.state.tasks.pop(payload)
        commit('deletedTask', true)
        commit('loadedTasks', true)
      }).catch(function (error) {
        console.error('Error removing document: ', error)
        commit('deletedTask', false)
      })
    }
  },
  // Getters: receive data from the store
  getters: {
    isAuthenticated (state) {
      return state.user !== null && state.user !== undefined
    }
  }
})

This is the component (Vue app -> Home -> Grid -> Tasklist (this):

<template>

  <v-card>

    <v-card-title primary class="title">{{title}}</v-card-title>

    <v-flex v-if="tasks.length > 0">
      <v-slide-y-transition class="py-0" group tag="v-list">
        <template v-for="(task, i) in tasks" v-if="task.quadrant === Id">
          <v-divider v-if="i !== 0" :key="`${i}-divider`"></v-divider>

          <v-list-tile :key="`${i}-${task.text}`">
            <v-list-tile-action>
              <v-checkbox v-model="task.done" color="info darken-3">
                <div slot="label" :class="task.done && 'grey--text' || 'text--primary'" class="text-xs-right" v-text="task.title +' ('+ task.taskId +')'"></div>
              </v-checkbox>
            </v-list-tile-action>
            <v-spacer></v-spacer>
            <v-scroll-x-transition>
              <v-btn flat icon color="primary" v-show="!deleteConfirmation" @click="deleteConfirmation = !deleteConfirmation">
                <v-icon>delete</v-icon>
              </v-btn>
            </v-scroll-x-transition>

            <v-scroll-x-transition>
              <v-btn flat icon color="green" v-show="deleteConfirmation" @click="deleteConfirmation = !deleteConfirmation">
                <v-icon>cancel</v-icon>
              </v-btn>
            </v-scroll-x-transition>

            <v-scroll-x-transition>
              <v-btn flat icon color="red" v-show="deleteConfirmation" @click="deleteConfirmation = !deleteConfirmation; deleteTask(task.taskId)">
                <v-icon>check_circle</v-icon>
              </v-btn>
            </v-scroll-x-transition>

          </v-list-tile>
        </template>
      </v-slide-y-transition>
    </v-flex>
  </v-card>
</template>

<script>

export default {
  data () {
    return {
      deleteConfirmation: false
    }
  },
  props: ['Id', 'title'],
  computed: {
    tasks () {
      return this.$store.state.tasks
    }
  },
  methods: {
    deleteTask (taskId) {
      console.log(taskId)
      this.$store.dispatch('deleteTask', taskId)
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
    <style scoped>
.container {
  max-width: 1200px;
}
</style>

Upvotes: 1

Views: 1211

Answers (2)

thimplicity
thimplicity

Reputation: 87

the DOM now updates on deletes properly. I changes the structure of the components a little bit. It is now: Vue app -> Home -> Grid -> Tasklist -> TaskListItem. I am still facing the following issue: Adding an item updates the DOM, but when I add multiple entries, e.g. with different titles, they are written to Firestore correctly, but in the DOM it shows them all as identical until I do a complete reload.

Please find the code below:

Store:

import Vue from 'vue'
import Vuex from 'vuex'
// import firebase from 'firebase'
import router from '@/router'
import db from '@/db'
import firebase from '@/firebase'

Vue.use(Vuex)

export const store = new Vuex.Store({
  state: {
    appTitle: 'My Awesome App',
    taskFormVisible: false,
    addedTask: false,
    loadedTasks: false,
    deletedTasks: false,
    user: null,
    error: null,
    loading: false,
    tasks: [],
    task: {
      title: '',
      description: '',
      tags: [],
      dueTime: '',
      dueDate: '',
      reminderFlag: Boolean,
      quadrant: '',
      userId: '',
      timestamp: ''
    }
  },
  // Mutations (synchronous): change data in the store
  mutations: {
    setUser (state, payload) {
      state.user = payload
      state.task.userId = firebase.auth().currentUser.uid
    },
    setError (state, payload) {
      state.error = payload
    },
    setLoading (state, payload) {
      state.loading = payload
    },
    addedTask (state, payload) {
      state.addedTask = payload
    },
    loadedTasks (state, payload) {
      state.loadedTasks = true
    },
    deletedTask (state, payload) {
      state.deletedTask = payload
    },
    DELETE_TASK (state, payload) {
      state.tasks = state.tasks.filter(task => {
        return task.taskId !== payload
      })
    },
    ADD_NEW_TASK (state, payload) {
      // algorithm to remove, e. g. by id
      // console.log(payload)
      state.tasks.push(payload)
      /* state.tasks = state.tasks.filter(e => {
        return e.id !== payload
      }) */
    },
    GET_TASKS (state, payload) {
      state.tasks = payload
    },
    MAKE_TASKFORM_VISIBLE (state, payload) {
      state.taskFormVisible = payload
    }

  },
  // Actions (asynchronous/synchronous): change data in the store
  actions: {
    autoSignIn ({ commit }, payload) {
      commit('setUser', { email: payload.email })
    },
    userSignUp ({ commit }, payload) {
      commit('setLoading', true)
      firebase.auth().createUserWithEmailAndPassword(payload.email, payload.password)
        .then(firebaseUser => {
          commit('setUser', { email: firebaseUser.user.email })
          commit('setLoading', false)
          router.push('/home')
          commit('setError', null)
        })
        .catch(error => {
          commit('setError', error.message)
          commit('setLoading', false)
        })
    },
    userSignIn ({ commit }, payload) {
      commit('setLoading', true)
      firebase.auth().signInWithEmailAndPassword(payload.email, payload.password)
        .then(firebaseUser => {
          commit('setUser', { email: firebaseUser.user.email })
          commit('setLoading', false)
          commit('setError', null)
          router.push('/home')
        })
        .catch(error => {
          commit('setError', error.message)
          commit('setLoading', false)
        })
    },
    userSignOut ({ commit }) {
      firebase.auth().signOut()
      commit('setUser', null)
      router.push('/')
    },
    makeTaskformVisible ({ commit }) {
      if (this.state.taskFormVisible === false) {
        commit('MAKE_TASKFORM_VISIBLE', true)
      } else {
        commit('MAKE_TASKFORM_VISIBLE', false)
      }
    },
    addNewTask ({ commit }, payload) {
      db.collection('tasks').add({
        title: payload.title,
        userId: firebase.auth().currentUser.uid,
        createdOn: firebase.firestore.FieldValue.serverTimestamp(),
        description: payload.description,
        dueDateAndTime: payload.dueTimestamp,
        reminderFlag: payload.reminderFlag,
        quadrant: payload.quadrant,
        tags: payload.tags,
        updatedOn: firebase.firestore.FieldValue.serverTimestamp()
      })
        .then(() => {
          commit('ADD_NEW_TASK', payload)
        })
        .catch(error => {
          commit('setError', error.message)
        })
    },
    getTasks ({ commit }) {
      // commit('loadedTasks', false)
      db.collection('tasks').orderBy('createdOn', 'desc').get().then(querySnapshot => {
        querySnapshot.forEach(doc => {
          const data = {
            'taskId': doc.id,
            'title': doc.data().title,
            'quadrant': doc.data().quadrant
          }
          this.state.tasks.push(data)
        })
        // commit('loadedTasks', true)
        commit('GET_TASKS', this.state.tasks)
      })
    },
    deleteTask ({ commit }, payload) {
      db.collection('tasks').doc(payload).delete().then(() => {
        commit('DELETE_TASK', payload)
      }).catch(function (error) {
        // console.error('Error removing document: ', error)
        commit('setError', error.message)
      })
    }
  },
  // Getters: receive data from the store
  getters: {
    isAuthenticated (state) {
      return state.user !== null && state.user !== undefined
    }
  }
})

TaskList:

<template>

  <ul>
    <li>
      <h4>{{title}}</h4>
    </li>
    <li v-for="task in tasks" :key="task.taskId" v-if="task.quadrant === Id" class="collection-item">
      <task-list-item v-bind:task="task"></task-list-item>
    </li>
  </ul>
</template>

<script>
import TaskListItem from './TaskListItem'

export default {
  components: {
    'task-list-item': TaskListItem
  },
  data () {
    return {

    }
  },
  props: ['Id', 'title'],
  computed: {
    tasks () {
      return this.$store.state.tasks
    }
  },
  methods: {

  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
    <style scoped>

</style>

TaskListItem:

<template>
  <div>{{task.title}} ({{task.taskId}})<v-spacer></v-spacer>
    <v-scroll-x-transition>
      <v-btn flat icon color="primary" v-show="!deleteConfirmation">
        <v-icon>edit</v-icon>
      </v-btn>
    </v-scroll-x-transition>
    <v-scroll-x-transition>
      <v-btn flat icon color="primary" v-show="!deleteConfirmation" @click="deleteConfirmation = !deleteConfirmation">
        <v-icon>delete</v-icon>
      </v-btn>
    </v-scroll-x-transition>

    <v-scroll-x-transition>
      <v-btn flat icon color="green" v-show="deleteConfirmation" @click="deleteConfirmation = !deleteConfirmation">
        <v-icon>cancel</v-icon>
      </v-btn>
    </v-scroll-x-transition>

    <v-scroll-x-transition>
      <v-btn flat icon color="red" v-show="deleteConfirmation" @click="deleteConfirmation = !deleteConfirmation; deleteTask(task.taskId)">
        <v-icon>check_circle</v-icon>
      </v-btn>
    </v-scroll-x-transition>
    </li>
  </div>

</template>

<script>

export default {
  data () {
    return {
      deleteConfirmation: false
    }
  },
  props: ['task'],
  computed: {

  },
  methods: {
    deleteTask (taskId) {
      this.$store.dispatch('deleteTask', taskId)
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
    <style scoped>

</style>

Upvotes: 0

Bennett Dams
Bennett Dams

Reputation: 7033

You have no mutation to actually change your tasks array, as e. g. this.state.tasks.pop(payload) happens in the deleteTask action, not the mutation.

The only way to actually change state in a Vuex store is by committing a mutation. https://vuex.vuejs.org/guide/mutations.html

That's also the reason why you see right result after reloading: The Firestore implementation is fine and updates its values. It will show the updated and newly fetched tasks array after reloading, but the Vuex state never gets changed before reloading, so Vue's displaying of the tasks array stays the same.

The solution to this is pretty straightforward:

Create a new mutation:

REMOVE_FROM_TASKS (state, payload) {
    // algorithm to remove, e. g. by id
    state.tasks = state.tasks.filter(e => {
        return e.id !== payload;
    });
},

Use this mutation in your action:

deleteTask ({ commit }, payload) {
  db.collection('tasks').doc(payload).delete().then(() => {
    commit('REMOVE_FROM_TASKS', payload)
    commit('deletedTask', true)
    commit('loadedTasks', true)
  }).catch(function (error) {
    console.error('Error removing document: ', error)
    commit('deletedTask', false)
  })
}

Upvotes: 2

Related Questions