Search code examples
typescriptvue.jsvuexquasar

Computed property setter is not being invoked


So I have a computed property which is an object of a type which I have defined in my Vuex Store. I'm trying to use this property on my component with v-model. I have defined getters and setters for this property and I'm pretty sure I have configured everything correctly unless I have overlooked something. I tried console logging inside of the setter to see if it ever fires up but nothing gets logged in the console so it isn't being called at all. Any help would be appreciated

code for Store:

import { Module, VuexModule, getModule, Mutation, Action } from 'vuex-module-decorators'
import { websocket } from 'src/boot/socket.io-client'
import store from 'src/store'
import { DataTablePagination } from '../types'
import { Course } from './types'
import { MessageModule } from 'src/store/message/index'
import { Notify } from 'quasar'
import { validate } from "class-validator"

export { Course } from './types'
export { DataTablePagination } from '../types'
export interface CourseState {
  pagination: DataTablePagination
  courses: Course []
  filter: string,
  disabled: boolean,
  selected: Course [],
  active: boolean,
  editCourseOpened: boolean,
  addCourseOpened: boolean,
  addCourseData: Course,
  editCourseData: Course,
  addCourseDisabled: boolean
}

@Module({
  name: 'course',
  namespaced: true,
  dynamic: true,
  store
})

class CourseClass extends VuexModule implements CourseState {
  public pagination: DataTablePagination = {
    descending: false,
    rowsNumber: 0,
    rowsPerPage: 10,
    page: 1,
    sortBy: 'name'
  }
  public courses: Course [] = []
  public filter = ''
  public disabled = true
  public selected: Course [] = []
  public active = true
  public editCourseOpened = false
  public addCourseOpened = false
  public addCourseData = new Course()
  public editCourseData = new Course()
  public addCourseDisabled = true

  @Mutation
  SET_ADDCOURSEDISABLED(disabled: boolean) {
    this.addCourseDisabled=disabled
  }

  @Mutation
  SET_EDITCOURSEDATA(data: Course) {
    this.editCourseData.copy(data)
  }

  // public SET_ADDCOURSEDATA(payload: { key: string, value: number | string }) {
  //   const { key, value } = payload
  //   if (Object.prototype.hasOwnProperty.call(this, key)) {
  //     // eslint-disable-next-line @typescript-eslint/no-explicit-any
  //     (this as any)[key] = value
  //   }
  // }
  @Mutation
  SET_ADDCOURSEDATA(data: Course) {
    this.addCourseData.copy(data)
  }

  @Mutation
  SET_EDITCOURSEOPENED(opened: boolean) {
    this.editCourseOpened=opened
  } 

  @Mutation
  SET_ADDCOURSEOPENED(opened: boolean) {
    this.addCourseOpened=opened
  }

  @Mutation
  SET_ACTIVE(active: boolean) {
    this.active=active
  }

  @Mutation
  SET_PAGINATION(pagination: DataTablePagination) {
    this.pagination=pagination
  }

  @Mutation
  SET_SELECTED(selected: Course []) {
    this.selected=selected
  }

  @Mutation
  SET_FILTER(filter: string) {
    this.filter=filter
  }

  @Mutation
  SET_COURSES(courses: Course []) {
    this.courses=courses
  }

  @Mutation
  SET_DISABLED(disabled: boolean) {
    this.disabled=disabled
  }

  @Action
  public ValidateAddCourseData() {
    validate(this.addCourseDisabled).then(errors => {
      console.log(errors)
      if(errors.length) {
        this.SET_ADDCOURSEDISABLED(true)
      } else {
        this.SET_ADDCOURSEDISABLED(false)
      }
    })
  }

  @Action 
  public async addCourse(input: Course) {
    websocket.emit('query', `mutation {
      createCourse (
        course: {
          code: "${input.code}"
          name: "${input.name}"
          creditHours: ${input.creditHours}
          numberOfLabs: ${input.numberOfLabs}
          contactHours: ${input.contactHours}
          chargeableCredits: 0
        }
      ) {
        ok
        message
      }
  }`, (response: { 
    errors: any
    data: { 
      createAcademicProgram: { 
        ok: boolean
        message: String 
      } 
    } 
  }) => {
      if(response.data) {
        this.fetchCourses()
        Notify.create({
          timeout: 3000,
          position: 'center',
          color: 'primary',
          message: 'Course Added Successfully'
        })
        this.SET_ADDCOURSEOPENED(!this.addCourseOpened)
      }
      else {
        MessageModule.newMessage({title: 'Error: Addition Failed', icon: 'error', message: 'Addition failed. Ensure that the course code you have entered is unique or contact your system administrator'})
      }
    })
  }

  @Action 
  public async deleteCourse(input: Course) {
    websocket.emit('query', `mutation {
      deleteCourse (
        courseId: "${input.id}"
      )  {
        id
        ok
        message
      }
    }`, (response: { 
      errors: any; 
      data: { 
        deleteCourse: { 
          id: any; 
          ok: boolean; 
          message: String 
        } 
      } 
    }) => {
      if(response.data) {
        this.fetchCourses()
        Notify.create({
          timeout: 2000,
          position: 'center',
          color: 'primary',
          message: 'Course Deleted Successfully'
        })
      }
      else {
        MessageModule.newMessage({ title: 'Deletion Failed', icon: 'error', message: 'Deletion Failed. Ensure that no course instances are still tied to this course or contact your system administrator' })
      }
    })
  }

  @Action
  public async editCourse(input: Course) {
    websocket.emit('query', `mutation {
      updateCourse (
        course: {
          id: "${input.id}"
          code: "${input.code}"
          name: "${input.name}"
          creditHours: ${input.creditHours}
          numberOfLabs: ${input.numberOfLabs}
          contactHours: ${input.contactHours}
          chargeableCredits: 0
        }
      ) {
        id
        ok
        message
      }
    }`, (response: {
      errors: any
      data: {
        updateCourse: {
          id: string
          ok: boolean
          message: string
        }
      }
    }) => {
      if(response.data) {
        this.fetchCourses()
        this.SET_SELECTED([input])
        this.SET_EDITCOURSEOPENED(!this.editCourseOpened)
        Notify.create({
          timeout: 3000,
          color: 'primary',
          message: 'Course Updated Successfully',
          position: 'center'
        })
      }
      else {
        MessageModule.newMessage({ title: 'Update Failed', icon: 'error', message: 'Update Failed. Ensure that all course codes are unique or contact your system administrator'})
      }
    })
  }

  @Action 
  public async fetchCourses() {
    const order = this.pagination.sortBy !== null ? `order: {
      by: ${this.pagination.sortBy}
      dir: ${this.pagination.descending ? 'DESC' : 'ASC'}
    }` : ''
    websocket.emit('query', `{
      courses(
        page: {
          skip: ${(this.pagination.page - 1) * this.pagination.rowsPerPage},
          first: ${this.pagination.rowsPerPage}
        }
        filter: {
          ilike: {
            name: "${this.filter}"
          }
        }
        ${order}
      ) {
        pagination {
          total
          listTotal
        }
        list {
          id
          code
          name
          creditHours
          numberOfLabs
          contactHours
        }
      }
    }`, (response: {
      errors: any
      data: {
        courses: {
          list: Course[]
          pagination: {
            total: number
            listTotal: number
          }
        }
      }
    }) => {
      this.SET_COURSES(response.data.courses.list)
      this.pagination.rowsNumber = response.data.courses.pagination.total
    })
  }
}

export const CourseModule = getModule(CourseClass)

Code for component:

<template>
  <q-dialog v-model="isOpened" :bordered="true">
    <q-card style="width: 50vw;">
      <q-toolbar class="bg-grey-5 text-center">
        <q-toolbar-title>Add Course</q-toolbar-title>
        <q-btn flat round dense icon="close" v-close-popup />
      </q-toolbar>
      <q-card-section class="col">
        <q-input v-model.trim="course.name" label="Name" type="object"
        :rules="[
          val => val.length > 0 || 'Required field'
        ]"/>
        <q-input v-model="course.code" label="Code"
        :rules="[
          val => val.length > 0 || 'Required field'
        ]"/>
        <q-input v-model.number="course.creditHours" label="Credit Hours" type="number"
        :rules="[
          val => val >= 0 || 'No negatives allowed'
        ]"/>
        <q-input v-model.number="course.numberOfLabs" label="Number of Labs" type="number"
        :rules="[
          val => val >= 0 || 'No negatives allowed'
        ]"/>
        <q-input v-model.number="course.contactHours" label="Contact Hours" type="number"
        :rules="[
          val => val >= 0 || 'No negatives allowed'
        ]"/>
        <q-card-actions align="right">
          <q-btn color="primary" :disable="disabled" @click="submit();">
            <q-icon name="save" />
            <q-tooltip
              anchor="top middle"
              self="bottom middle"
              :offset="[10, 10]"
            >
              <strong>Add Course</strong>
            </q-tooltip>
          </q-btn>
          <q-btn color="red" @click="toggle()" icon="cancel">
            <q-tooltip
            anchor="top middle"
            self="bottom middle"
            :offset="[10, 10]"
            >
              <strong>Cancel</strong>
            </q-tooltip>
          </q-btn>
        </q-card-actions>
      </q-card-section>
    </q-card>
  </q-dialog>
</template>

<script lang="ts">
import { Vue, Component, Watch } from 'vue-property-decorator'
import { CourseModule, Course } from 'src/store/course/index'

@Component
export default class AddCourse extends Vue {
  //private course: Course = new Course()
  //private disabled = true

  get disabled() {
    return CourseModule.addCourseDisabled
  }

  get course() {
    return CourseModule.addCourseData
  }

  set course(args: any) {
    console.log(args)
    //CourseModule.SET_ADDCOURSEDATA(newValue)
  }

  get isOpened() {
    return CourseModule.addCourseOpened
  }

  set isOpened(newValue: boolean) {
    CourseModule.SET_ADDCOURSEOPENED(newValue)
  }

  toggle() {
    CourseModule.SET_ADDCOURSEDATA(this.course)
    this.isOpened = !this.isOpened
    // if(this.isOpened) {
    //   this.getdisabled()
    // }
    // else {
    //   this.course.clear()
    // }
  }

  submit() {
    CourseModule.addCourse(this.course)
    //this.toggle()
  }

  // getdisabled() {
  //   validate(this.course).then(errors => {
  //     if(errors.length) {
  //       this.disabled=true
  //     } else {
  //       this.disabled=false
  //     }
  //   })
  //   return true
  // }
}
</script>

Solution

  • As you've already noted, the standard solution here would be to have a computed property for each form field, with get and set for each one passing through to the store.

    One viable alternative would be to use a Proxy to intercept the setting of the object properties. I don't know how to write this in TypeScript but that shouldn't detract from the core idea:

    const store = new Vuex.Store({
      state: {
        course: {
          name: 'Algebra',
          teacher: 'Smith'
        }
      },
      
      mutations: {
        course (state, course) {
          state.course = course
        }
      }
    })
    
    new Vue({
      el: '#app',
      store,
      
      computed: {
        course () {
          const store = this.$store
          const course = store.state.course
          
          return new Proxy(course, {
            set (obj, key, value) {
              store.commit('course', { ...obj, [key]: value })          
              return true
            }
          })
        }
      }
    })
    <script src="https://unpkg.com/[email protected]/dist/vue.js"></script>
    <script src="https://unpkg.com/[email protected]/dist/vuex.js"></script>
    
    <div id="app">
      <input v-model="course.name">
      <br>
      <input v-model="course.teacher">
      <br>
      <p>{{ course.name }}</p>
      <p>{{ course.teacher }}</p>
    </div>

    If you need to support IE then you won't be able to use Proxy but it should be possible to implement something similar using standard JavaScript properties. The idea is much the same as for the proper Proxy but you'd have to explicitly create get and set handlers on the 'proxy' object.