Vue Keys do not delete from Object - vue.js

I'm trying to delete a key from an object in a parent component. A child component emits an event (with an item value) back to the parent method that triggers the delete in the parent's data object.
Parent component:
data() {
return {
savedNews: Object
}
},
methods: {
containsKey(obj, key) {
var result = Object.keys(obj).includes(key)
return result
},
handleSaveNews(item) {
if (!this.containsKey(this.savedNews, item.url)) {
this.savedNews = {
[item.url]: item,
...this.savedNews
}
} else {
console.log(this.containsKey(this.savedNews, item.url))
var res = delete(this.savedNews, item.url)
console.log(res)
console.log(this.containsKey(this.savedNews, item.url))
}
}
}
All of the console.logs in the last else statement return true. It's saying that the delete was successful yet the key is still there. How do I delete this key?

From the docs:
Vue cannot detect property addition or deletion
Use this.$delete:
this.$delete(this.savedNews, item.url)
or this.$set (which also should be used for property changes):
this.$set(this.savedNews, item.url, undefined);
Extra info: The $ is a naming convention Vue uses for its built-in methods that are available on each component instance. There are some plugins which opt to follow this pattern too. You can also use built-ins inside other modules if you import Vue and use Vue.delete, for example. You could add your own methods like Vue.prototype.$mymethod = ....

Related

How to avoid rerendering all child components which are created by v-for directive

There is a list of child component
<question-list-item
v-for="(item, index) in questionListParsed"
:key="item.id"
:both-question="item"
:class-id="classId"
:subject-id="subjectId"
:index="index+1"
/>
and the questionListParsed is a getter in vuex.
/**************************************************************************
* getters
**************************************************************************/
get questionListParsed(): QuestionListItemRes[] {
const { questionList, showingOriginalQuestion } = this
const questionListParsed = questionList.map((e) => {
const recommendQuestion = e.recommendedQuestions[0]
const recommendQuestionIds = showingOriginalQuestion[e.questionNumber]
let arr = []
if (recommendQuestionIds) {
arr = recommendQuestionIds.filter((item) => {
return !this.removedRecommendQuestionIds.includes(item)
})
}
return {
recommendQuestion: {
...recommendQuestion,
stem: recommendQuestion.question,
knowledges: splitMultiKnowledge(recommendQuestion.knowledge),
questionSourceList: recommendQuestion.sources,
categoryId: recommendQuestion.categoryId,
},
originalQuestion: {
...e,
id: e.questionNumber,
stem: e.question,
difficulty: e.complexity,
knowledges: splitMultiKnowledge(e.knowledge),
},
id: recommendQuestion.id,
questionSimilarId: e.questionNumber,
mistakeAnswerId: e.id,
targetExerciseId: e.targetExerciseId,
status: recommendQuestion.status,
}
})
return questionListParsed
}
and the questionListParsed is mainly depends on the state questionList whitch is the originnal data from server side. Now i change questionList by the following way
#Mutation
updateQuestionListByIndex(data: UpdateParams): void {
if (data.value) {
const temp = [...this.questionList]
temp[data.index] = data.value
this.questionList = temp
}
}
and commit the mutation inside an Action like these
this.context.commit('updateQuestionListByIndex', {
index: targetIndex,
value: originQuestion[0],
})
I just want to change one item in the array questionList and then questionListParsed changed.
The expectation is that only one component updated but all of the child component updated(use console.log('updated') in its updated hocks).
How to do that?
The reason why all components are updated is because you use computed property (Vuex getters are Vue computed properties).
Whenever anything in questionList is changed, questionListParsed is recomputed and because you are using map and generating new objects, the result is a new array with completely new objects --> every child in list is updated
I would not consider it a problem because in reality only the DOM elements of the changed item are updated (that is the beauty of virtual DOM). If you do see some performance problem, the way around it is to stop using computed/getters and instead do the transformation only once when data is loaded and continue to work only with questionListParsed
You don't need to prevent the child components from rerendering, Vue does that for you. By providing a unique key to each list element :key="item.id" you give Vue a hint about the item, so Vue can identify and reuse the already rendered parts.
See https://v2.vuejs.org/v2/api/#key for more information.

What is affecting on will Vue computed property re-computed or no?

I expect that currentSelectionViewContent will be re-computed each time when selectedOptionsIndexes has changed. Really, sometimes it works, sometimes - not.
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
#Component({
template
})
export class SelectField extends Vue {
private onNewOptionSelected(newOption: SelectField.Option, indexInArray: number): void {
console.log("~~~~~~~~~~~~~~~");
console.log(JSON.stringify(this.selectedOptionsIndexes, null, 2));
this.selectedOptionsIndexes[0] = indexInArray;
console.log(JSON.stringify(this.selectedOptionsIndexes, null, 2));
console.log("--------------");
if (isUndefined(newOption.key)) {
this.$emit("change", newOption.relatedEntity);
} else {
this.$emit("change", newOption.key);
}
}
// Vue computed property in "vue-property-decorator" syntax
private get currentSelectionViewContent(): string {
console.log("Recomputing ...");
switch (this.selectedOptionsIndexes.length) {
case 0:
return SelectField.DEFAULT_NOTHING_SELECTED_PLACEHOLDER;
case 1:
return this.selectOptions[this.selectedOptionsIndexes[0]].title;
default:
return SelectField.DEFAULT_MULTIPLE_OPTIONS_SELECTED_LETTERING;
}
}
}
Working case:
Not working case (no re-computing):
I sorry about was not created the repro for this case (the reproducing of component where this problem causes, it's dependencies and also environment) takes too long time. If you can not understand what wrong here without repro, please just teach me what is affecting on will Vue computed property re-computed or no.
Vue has certain behavior around arrays to be aware of. From the docs:
Vue cannot detect the following changes to an array:
When you directly set an item with the index, e.g.
vm.items[indexOfItem] = newValue
When you modify the length of the
array, e.g. vm.items.length = newLength
To ensure Vue sees your array change, always make a copy of the array and re-assign it, like this:
var updatedIndexes = [...this.selectedOptionsIndexes]; // Copies array
updatedIndexes[0] = indexInArray; // Update the copy
this.selectedOptionsIndexes = updatedIndexes; // Overwrite with copy

How to automatically construct watch property based on data attributes in Vue.js?

I have standard Vue.js component and I'd like to convert attributes in data property to watcher or in other words I want to construct a watch object based on data property attributes automatically
my idea looks something like this
watch: {
...(() => {
const watchers = {}
Object.keys(this.$data).forEach(key => {
watchers[key] = () => {
new ProductNutrientUpdate(this).run()
}
})
return watchers
})(),
},
the problem with this approach is that this.$data is not constructed yet
maybe there is some way how I can add watchers in created hook for example??
Vue already watches properties of the data object (note if any of these values are themselves objects, I think you need to update the whole object, i.e. change its value to a shallow copy with the desired nested key-values).
Refer to: https://v2.vuejs.org/v2/guide/reactivity.html
You can then use the update lifecycle hook to watch for all changes to data: https://v2.vuejs.org/v2/api/#updated
I was able to resolve a challenge using the following approach
created() {
Object.keys(this.$data).forEach(key => {
this.$watch(key, function() {
// ... some logic to trigger on attribute change
})
})
}

How to watch child properties changes from parent component

I am using a date picker component library and i want to watch when a property of that component changes.
I have tried this:
watch: {
'$refs.picker.popupVisible': {
handler (new_value) {
console.log('executed')
},
deep: true
}
}
Also this:
computed: {
picker () {
console.log(this.$refs.picker.popupVisible)
return this.$refs.picker.popupVisible
}
}
I know that the solution will be a vue.js hack because this is not the right way.If i had access to child component i would emit en event to parent but unfortunately i don't have.
I had a similar problem using a library which had some limitations.
Unfortunately, your watcher will not work.You have to use the function watcher to make this to work.And you have to use it inside the mounted hook.
mounted() {
this.$watch(
"$refs.picker.popupVisible",
(new_value, old_value) => {
//execute your code here
}
);
}
I also have an example. Please take a look here
What you can do is create a data object in parent component and include the date field in that data object and pass that data object to child component as props
<child :dateObj="dateObj"></child>
data: {
dateObj: {
date: ""
}
}
And in child component you can use the date field of that dateObj props. This is possible because Vue doesn't watch the property of Objects passed as props and we can modify them without Vue complaining in console.
Thus the changed date field is reflected in parent as well.

UI not updating when nested array property value deleted, only when added

I have a page where an object with nested array values are passed in from the parent component. The user can then, using a series of events and components manage the data in these subscriptions. Currently I'm facing an issue where when a subscriptionId is removed from the props, conditions on the page aren't changing, but they do when it's added.
Child Component
export default {
props: {
// Format of this object is:
// { "gameId": [
// 'subscriptionId',
// 'subscriptionId',
// ] }
subscriptions: {
type: Object,
required: true
}
},
watch: {
subscriptions: {
handler: function (newSubscriptions, oldSubscriptions) {
// NEVER gets fired when `subscriptionId` deleted from array list, but is fired when a new subscription is added
console.log('handler');
}
},
deep: true
}
},
I suspect this might be related to how I'm removing the array from the object. Essentially I'm copying the array, deleting the index in question and overwriting the original array. My hope with this approach is that the watcher wouldn't be needed but it appears to have no impact. Here's the code that exists on the parent component to update the subscriptions:
Parent Component
// Works great, don't have any issues here
handleSubscribed (subscriptionId) {
let newSubscriptions = [subscriptionId];
if (this.subscriptions.hasOwnProperty(this.currentGame.id)) {
newSubscriptions = this.subscriptions[this.currentGame.id];
newSubscriptions.push(subscriptionId);
}
this.$set(this.subscriptions, this.currentGame.id, newSubscriptions);
},
handleUnsubscribed (subscriptionId) {
// if there's more than one, delete only the one we're looking for
if (this.subscriptions.hasOwnProperty(this.currentGame.id) && this.subscriptions[this.currentGame.id].length > 1) {
let newSubscriptions = this.subscriptions[this.currentGame.id];
delete newSubscriptions[newChannels.indexOf(subscriptionId)];
this.$set(this.subscriptions, this.currentGame.id, newSubscriptions);
// shows my subscription has been removed, but GUI doesn't reflect the change
console.log('remove-game', newSubscriptions);
return;
}
this.$delete(this.subscriptions, this.currentGame.id);
},
I was hoping watch might be the solution, but it's not. I've looked over the reactive docs several times and don't see a reason for why this wouldn't work.
VueJS version: 2.5.7
Use Vue.delete instead of the delete keyword.
The object is no longer observable when using delete, therefore not reactive.
Delete a property on an object. If the object is reactive, ensure the deletion triggers view updates. This is primarily used to get around the limitation that Vue cannot detect property deletions, but you should rarely need to use it.