Vue2 composition API array property update not updating - vuejs2

I have this component that accepts an array as a property:
import {
defineComponent,
getCurrentInstance,
toRefs,
watch,
} from "#vue/composition-api";
import { RecommendationAnswer, RecommendationQuestion } from "#models";
import { useCalculateInitialCount } from "./calculate-count";
import { useGetAnsweredQuestions } from "./list-questions";
export default defineComponent({
name: "StepThree",
emits: ["onSelect"],
props: {
products: {
type: Array,
required: false,
default: () => [],
},
questions: {
type: Array,
required: false,
default: () => [],
},
},
setup(props) {
const instance = getCurrentInstance();
const { products, questions } = toRefs(props);
watch(
products,
(currentProducts: any[]) => {
if (!currentProducts) return;
const currentQuestions = <RecommendationQuestion[]>questions.value;
useCalculateInitialCount(currentProducts, currentQuestions);
},
{
immediate: true,
}
);
const selectAnswer = (answer: RecommendationAnswer) => {
answer.selected = !answer.selected;
questions.value.forEach((question: RecommendationQuestion) => {
question.selected = !!question.answers.find(
(item: RecommendationAnswer) => item.selected
);
});
const answeredQuestions = useGetAnsweredQuestions(
<RecommendationQuestion[]>questions.value
);
instance.proxy.$emit("onSelect", {
step: 3,
questions: answeredQuestions,
});
};
return { selectAnswer };
},
});
The watch is triggered whenever the products array changes (which happens outside of this component).
I can see that the watch fires and then the function useCalculateInitialCount fires, which updates the count property on an answer.
This is displayed in the template:
<v-col cols="6">
<base-fade-up class="row" :duration="0.1" tag="div">
<v-col
class="text-center"
cols="12"
v-for="question in questions.slice(
0,
Math.ceil(questions.length / 2)
)"
:key="question.id"
>
{{ question.title }}
<v-card
class="w-100"
outlined
#click="selectAnswer(answer)"
v-for="answer in question.answers"
:key="answer.id"
>
<v-card-text class="text-center">
{{ answer.title }} ({{ answer.count }})
</v-card-text>
</v-card>
</v-col>
</base-fade-up>
</v-col>
When the component loads, the watch fires and the counts are displayed correctly:
But when the products update, even though I see the changes in the console.log:
The template does not update.
Does anyone know how I can get around this?

I think it's because your array does not have a new item, so for the watcher is the same array with the same amount of items even if one of them has changed. I'm not sure why you have to watch a property but if you need to watch all the changes in the array you can try to make a copy of the array first and then watch that copied array

I figured a work around for this, by created a computed property instead of watching the products.
The entire code looks like this:
import {
computed,
defineComponent,
getCurrentInstance,
toRefs,
watch,
} from "#vue/composition-api";
import { RecommendationAnswer, RecommendationQuestion } from "#models";
import { useCalculateInitialCount } from "./calculate-count";
import { useGetAnsweredQuestions } from "./list-questions";
export default defineComponent({
name: "StepThree",
emits: ["onSelect"],
props: {
products: {
type: Array,
required: false,
default: () => [],
},
questions: {
type: Array,
required: false,
default: () => [],
},
},
setup(props) {
const instance = getCurrentInstance();
const { products, questions } = toRefs(props);
const questionsWithCount = computed(() => {
const currentProducts = <any[]>products.value;
const currentQuestions = [...(<RecommendationQuestion[]>questions.value)];
if (!currentProducts?.length || !currentQuestions?.length) return;
useCalculateInitialCount(currentProducts, currentQuestions);
return currentQuestions;
});
const selectAnswer = (answer: RecommendationAnswer) => {
answer.selected = !answer.selected;
questions.value.forEach((question: RecommendationQuestion) => {
question.selected = !!question.answers.find(
(item: RecommendationAnswer) => item.selected
);
});
const answeredQuestions = useGetAnsweredQuestions(
<RecommendationQuestion[]>questions.value
);
instance.proxy.$emit("onSelect", {
step: 3,
questions: answeredQuestions,
});
};
return { questionsWithCount, selectAnswer };
},
});
This fixed the issue, because in the template I use the questionsWithCount instead of the questions

Related

Problems with chart.js redrawing or not redrawing graphs

I am Japanese. Therefore, my sentences may be strange. Please keep that in mind.
I am writing code using vue.js, vuex, vue-chart.js and vue-chart.js to display the population of each prefecture of Japan when checked.I’m code is written to redraw the graph when the input element for each prefecture is checked.However, it does not redraw when checked.Also, it may redraw after half of the check.I believe this phenomenon can be confirmed from the following URL.
https://yumemi-coding.web.app/
※There are no errors.
Here's a question: what causes the graphs to redraw or not? Also, how can I code to remedy this?
What I have done to counteract the cause is as follows
I went to the official website and used the rendering process as a reference.
 URL:https://vue-chartjs.org/migration-guides/#new-reactivity-system
 => The way we did it was right.
We thought there was a problem with VueX and coded in a way that did not use it. => There was nothing wrong with vuex.
TopFroont.vue
<template>
<div class="Bar_area">
<Bar :options="chartOptions" :data="chartData" class="Bar_item" />
</div>
</template>
<script>
import { Bar } from "vue-chartjs"
import { Chart as ChartJS, registerables } from "chart.js"
ChartJS.register(...registerables)
export default {
name: "BarChart",
components: { Bar },
data() {
return {
chartOptions: {
responsive: true,
},
}
},
computed: {
chartData() {
return {
labels: this.$store.state.years,
datasets: this.$store.state.prefectures,
}
},
},
}
</script>
NaviBar.vue
<template>
<div class="navApp">
<ul>
<li v-for="(pref, index) in prefData" :key="index" class="pref_itemBox">
<label>
<input type="checkbox" #change="checkItem(pref)" />
<span class="pref_text">{{ pref.prefName }}</span>
</label>
</li>
</ul>
</div>
</template>
<script>
import resasInfo from "#/library/resas.js"
import axios from "axios"
export default {
data() {
return {
resasInfo: resasInfo,
url: resasInfo.url_prefectures,
api: resasInfo.api,
prefData: [],
prefectures: [],
}
},
async created() {
const request_Header = {
headers: { "X-API-KEY": this.api.key },
}
await axios.get(this.url, request_Header).then((res) => {
const value = res.data.result
this.prefData.push(...value)
})
},
methods: {
checkItem(pref) {
// チェックされてる都道府県のみを配列に入れる
const isExistencePref = this.prefectures.indexOf(pref)
isExistencePref === -1
? this.prefectures.push(pref)
: this.prefectures.splice(isExistencePref, 1)
this.$store.dispatch("getPrefectures", this.prefectures)
},
},
}
</script>
vuex => store/index.js
import axios from "axios"
import { createStore } from "vuex"
import createPersistedState from "vuex-persistedstate"
export default createStore({
state: {
prefectures: [],
years: [],
},
mutations: {
getPrefs(state, payload) {
state.prefectures = payload
},
getYears(state, payload) {
state.years = payload
},
},
actions: {
getPrefectures({ commit }, payload) {
// payload => 各都道府県のprefCode + prefName
const allPrefecture_Data = []
const result = payload.map(async (el) => {
const prefCode_data = el.prefCode
axios
.get(
`https://opendata.resas-portal.go.jp/api/v1/population/composition/perYear?prefCode=${prefCode_data}&cityCode=-`,
{
headers: {
"X-API-KEY": "5RDiLdZKag8c3NXpEMb1FcPQEIY3GVwgQwbLqFIx",
},
}
)
.then((res) => {
const value = res.data.result.data[0].data
const TotalPopulation_Year = []
const TotalPopulation_Data = []
// 都道府県の総人口データと年データを各配列に入れ込む
value.forEach((element) => {
TotalPopulation_Data.push(element.value)
TotalPopulation_Year.push(element.year)
})
// rgbaを自動生成する関数 => backgroundColor
const generateRGBA = () => {
const r = Math.floor(Math.random() * 256)
const g = Math.floor(Math.random() * 256)
const b = Math.floor(Math.random() * 256)
const a = 0.8
return `rgba(${r}, ${g}, ${b}, ${a})`
}
// chart.jsに入れ込むデータ
const prefData = {
label: el.prefName,
data: TotalPopulation_Data,
backgroundColor: generateRGBA(),
}
allPrefecture_Data.push(prefData)
commit("getPrefs", allPrefecture_Data)
commit("getYears", TotalPopulation_Year)
})
.catch((err) => {
console.log(err)
})
})
return result
},
},
plugins: [createPersistedState()],
getters: {},
modules: {},
})

Testing vue watchers with vue-testing-library

Anyone know how I would test a watcher in a component with vue-testing-library?
Here is my component. I want to test that the method is called when the brand vuex state is updated. With vue test utils it would be easy but I have not found a good way to do this with vue testing library. Has anyone did this before using vue testing library.
<template>
<v-data-table
data-testid="builds-table"
:headers="headers"
:items="builds"
:items-per-page="10"
class="elevation-1"
:loading="loading"
>
<template v-slot:[getItemStatus]="{ item }">
<v-chip :color="getStatusColor(item.status)" dark>
{{ item.status }}
</v-chip>
</template>
</v-data-table>
</template>
<script>
import { mapState } from "vuex";
import { getScheduledBuilds } from "../services/buildActivationService";
import { getStatusColor } from "../utils/getStatusColor";
export default {
name: "BuildsTable",
data() {
return {
loading: false,
headers: [
{
text: "Activation Time",
align: "start",
value: "buildActivationTime",
},
{ text: "Build ID", value: "buildId" },
{ text: "Build Label", value: "buildLabel" },
{ text: "Status", value: "status" },
],
error: "",
};
},
async mounted() {
this.getBuilds();
},
computed: {
...mapState(["brand", "builds"]),
getItemStatus() {
return `item.status`;
},
},
watch: {
brand() {
this.getBuilds();
},
},
methods: {
getStatusColor(status) {
return getStatusColor(status);
},
async getBuilds() {
try {
this.loading = true;
const builds = await getScheduledBuilds(this.$store.getters.brand);
this.$store.dispatch("setBuilds", builds);
this.items = this.$store.getters.builds;
this.loading = false;
} catch (error) {
this.loading = false;
this.error = error.message;
this.$store.dispatch("setBuilds", []);
}
},
},
};
</script>
Vue Testing Library is just a wrapper for Vue Test Utils, so the same call verification techniques apply.
Here's how to verify the call with Jest and Vue Testing Library:
Spy on the component method definition before rendering the component:
import { render } from '#testing-library/vue'
import BuildsTable from '#/components/BuildsTable.vue'
const getBuilds = jest.spyOn(BuildsTable.methods, 'getBuilds')
render(BuildsTable)
Render the component with a given store and a callback to capture the Vuex store instance under test:
let store = {
state: {
brand: '',
builds: [],
}
}
const storeCapture = (_, vuexStore) => store = vuexStore
render(BuildsTable, { store }, storeCapture)
Update the store's brand value, and wait a macro tick for the watcher to take effect, then verify the getBuilds spy is called twice (once in mounted() and again in the brand watcher):
store.state.brand = 'foo'
await new Promise(r => setTimeout(r)) // wait for effect
expect(getBuilds).toHaveBeenCalledTimes(2)
The full test would look similar to this:
import { render } from '#testing-library/vue'
import BuildsTable from '#/components/BuildsTable.vue'
describe('BuildsTable.vue', () => {
it('calls getBuilds when brand changes', async() => {
const getBuilds = jest.spyOn(BuildsTable.methods, 'getBuilds')
let store = {
state: {
brand: '',
builds: [],
}
}
const storeCapture = (_, vuexStore) => store = vuexStore
render(BuildsTable, { store }, storeCapture)
store.state.brand = 'foo'
await new Promise(r => setTimeout(r)) // wait for effect
expect(getBuilds).toHaveBeenCalledTimes(2)
})
})

Editing tasks in vue

cannot edit list
I think the problem is in the update mutation.
Everything is fine with the label, probably a problem with input, it does not update the data.
I need to make the tasks can be edited on a double wedge.
cannot edit list
I think the problem is in the update mutation.
Everything is fine with the label, probably a problem with input, it does not update the data.
I need to make the tasks can be edited on a double wedge.
Vue.use(Vuex)
export default new Vuex.Store({
state: {
todos: localData().get()
},
mutations: {
editTodo: (state, id) => {
let todo = state.todos.find(todo =>
(todo.id === id))
todo.edit = true
localData().set(state.todos)
}, //mutations editTodo
update: (state, id, newEvent) => {
let todo = state.todos.find(todo =>
(todo.id === id))
todo.title = newEvent
todo.edit = false
localData().set(state.todos)
},
},
})
<template>
<li>
<label
v-if="!edit"
#dblclick="editTodo"
>
{{ title }}
</label>
<input
v-else
class="edit"
type="text"
:value="newEvent" //it seems he is interrupting the title
#keyup.enter="update"
>
</li>
</template>
<script>
export default {
name: 'todo',
props: ['id', 'title', 'edit', 'completed'],
data() {
return {
newEvent: '' //Am I doing the right thing to add newEvent?
}
},
computed: {
todos() {
return this.$store.state.todos
}
},
methods: {
editTodo() {
this.$store.commit('editTodo', this.id)
},
update() {
this.$store.commit('update', this.id, this.newEvent) //update method
},
}
}
First, let's define what is wrong in your code. You're updating Vuex state object using update function but you're giving :value="newEvent" which is in your component, so Vuex doesn't see this. First, create state data and getters for newEvent
state:{
//..
newEvent: ""
}
getters:{
newEvent: state => state.newEvent
}
Then use this state element in your component
// ..
import { mapGetters } from "vuex"
// ..
computed:{
...mapGetters(["newEvent"])
}
You should use logic like that

Detect vuex state change to execute a method inside a nuxt layout

I am trying to show vuetify snackbar alert, once I completed a form submission inside a page or vue component. I use vuex store to manage alert type and message.
my-nuxt-app/store/alerts.js
export const state = () => ({
message: '',
type: ''
});
export const getters = {
hasAlert(state) {
return state.message !== '';
},
alertMessage(state) {
return state.message;
},
alertType(state) {
return state.type;
}
};
export const mutations = {
SET_ALERT(state, payload) {
state.type = payload.type;
state.message = payload.message;
}
};
export const actions = {
setAlert({commit}, payload) {
commit('SET_ALERT', payload);
},
clearAlert({commit}) {
commit('SET_ALERT', {});
}
};
And I created a nuxt plugin to access getters globally in my application.
my-nuxt-app/plugins/alert.js
import Vue from 'vue';
import {mapGetters} from 'vuex';
const Alert = {
install(Vue, options) {
Vue.mixin({
computed: {
...mapGetters({
hasAlert: 'alerts/hasAlert',
alertType: 'alerts/alertType',
alertMessage: 'alerts/alertMessage'
})
}
});
}
};
Vue.use(Alert);
Inside my AccountForm component submit method, I am dispatching my alert information to store like below.
my-nuxt-app/components/form/AccountForm.vue
...
methods: {
async submit () {
try {
await this.$axios.patch("/settings/profile", this.form);
this.$store.dispatch('alerts/setAlert', {
type: 'success',
message: 'You have successfully updated your information.'
});
} catch (e) {
}
}
},
...
}
...
And this AccountForm.vue component is a child component of profile.vue page which is obviously inside the pages folder of my project. And also I have extended the dashboard.vue layout to this profile.vue page and to the most of the pages inside my pages directory as a common layout. Hence, I added the snackbar component into dashboard layout to show a alert message whenever required.
my-nuxt-app/layouts/dashboard.vue
<template>
...
<v-snackbar
:timeout="snackbar.timeout"
:color="snackbar.color"
:top="snackbar.y === 'top'"
:bottom="snackbar.y === 'bottom'"
:right="snackbar.x === 'right'"
:left="snackbar.x === 'left'"
:multi-line="snackbar.mode === 'multi-line'"
:vertical="snackbar.mode === 'vertical'"
v-model="snackbar.show"
>
{{ snackbar.text }}
<v-btn flat icon dark #click.native="snackbar.show = false">
<v-icon>close</v-icon>
</v-btn>
</v-snackbar>
...
</template>
<script>
...
data: () => ({
snackbar: {
show: false,
y: 'top',
x: null,
mode: '',
timeout: 6000,
color: '',
text: ''
},
}),
computed: {
availableAlert: function () {
return this.hasAlert;
}
},
watch: {
availableAlert: function(alert) {
if(alert) {
this.showAlert(this.alertType, this.alertMessage);
this.$store.dispatch('alerts/clearAlert');
}
}
},
methods: {
showAlert(type, message) {
this.snackbar.show = true;
this.snackbar.color = type;
this.snackbar.text = message;
}
}
</script>
I am getting the alert message for the first time submission of the form and after that I have to reload the page and then submit to get the alert. Please enlighten me a way to detect the vuex state change and trigger showAlert method inside the dashboard.vue accordingly.
It's most likely the way you're checking hasAlert
Your clearAlert passes an empty object, your setAlert is trying to assign properties of that empty object, while your hasAlert is checking if it's an empty string.
If you change your clearAlert to:
clearAlert({commit}) {
commit('SET_ALERT', { message: '', type: '' });
}
That should fix your issue.

Getting Error in render: "TypeError: Cannot read property 'title' of undefined" when rendering CourseDescriptionPageComponent

Here is how CourseDescriptionPage.vue looks
import CourseCover from './CourseDescription/CourseCover.vue'
import WhyJoin from './CourseDescription/WhyJoin.vue'
import CourseStructure from './CourseDescription/CourseStructure.vue'
export default {
props: ['id'],
data () {
return {
hasDetails: false
}
},
created () {
this.$store.dispatch('loadCourseDetails', this.id).then(() => {
this.hasDetails = true
})
},
computed: {
course () {
return this.$store.state.courseDetails[this.id]
}
},
components: {
CourseCover,
WhyJoin,
CourseStructure
},
name: 'CourseDescriptionPage'
}
<template>
<div v-if="hasDetails">
<course-cover :courseTitle="course.title" :courseDuration="course.duration"></course-cover>
<why-join :courseTitle="course.title" :courseJobs="course.jobs"></why-join>
<course-structure :lectureList="course.lectureList"></course-structure>
</div>
</template>
Here is how my store looks
import Vuex from 'vuex'
import * as firebase from 'firebase'
Vue.use(Vuex)
export const store = new Vuex.Store({
state: {
courseDetails: {},
loading: false
},
mutations: {
setCourseDetails (state, payload) {
const { id, data } = payload
state.courseDetails[id] = data
},
setLoading (state, payload) {
state.loading = payload
}
},
actions: {
loadCourseDetails ({commit}, payload) {
commit('setLoading', true)
firebase.database().ref(`/courseStructure/${payload}`).once('value')
.then((data) => {
commit('setCourseDetails', {
id: payload,
data: data.val()
})
commit('setLoading', false)
})
.catch(
(error) => {
console.log(error)
commit('setLoading', false)
}
)
}
}
Here is how my CourseCover.vue looks
export default {
props: {
courseTitle: {
type: String,
required: true
},
courseDuration: {
type: String,
required: true
}
},
name: 'CourseCover'
}
<template>
<v-jumbotron
src="./../../../static/img/course_cover_background.png">
<v-container fill-height>
<v-layout align-center>
<v-flex>
<h3>{{ courseTitle }}</h3>
<span>{{ courseDuration }}</span>
<v-divider class="my-3"></v-divider>
<v-btn large color="primary" class="mx-0" #click="">Enroll</v-btn>
</v-flex>
</v-layout>
</v-container>
</v-jumbotron>
</template>
I think there is something wrong with the way I am using props here but I couldn't figure out.
The data is loaded in store by the firebase that I know for sure because it shows in Vue dev tools but I just couldn't understand why Vue is complaining about that.
Thanks in advance.
course is undefined on component initialize ,so then you should return an empty object:
computed: {
course () {
return this.$store.state.courseDetails[this.id] || {}
}
},