Computed not reactive? - vue.js

I wrote this code to return a list of skills. If the user already has a specific skill, the list-item should be updated to active = false.
This is my initial code:
setup () {
const user = ref ({
id: null,
skills: []
});
const available_skills = ref ([
{value: 'css', label: 'CSS', active: true},
{value: 'html', label: 'HTML', active: true},
{value: 'php', label: 'PHP', active: true},
{value: 'python', label: 'Python', active: true},
{value: 'sql', label: 'SQL', active: true},
]);
const computed_skills = computed (() => {
let result = available_skills.value.map ((skill) => {
if (user.value.skills.map ((sk) => {
return sk.name;
}).includes (skill.label)) {
skill.active = false;
}
return skill;
});
return result;
})
return {
user, computed_skills
}
},
This works fine on the initial rendering. But if I remove a skill from the user doing
user.skills.splice(index, 1) the computed_skills are not being updated.
Why is that the case?

In JavaScript user or an object is a refence to the object which is the pointer itself will not change upon changing the underling properties hence the computed is not triggered
kid of like computed property for an array and if that array get pushed with new values, the pointer of the array does not change but the underling reference only changes.
Work around:
try and reassign user by shadowing the variable

The computed prop is actually being recomputed when you update user.skills, but the mapping of available_skills produces the same result, so there's no apparent change.
Assuming user.skills contains the full skill set from available_skills, the first computation sets all skill.active to false. When the user clicks the skill to remove it, the re-computation doesn't set skill.active again (there's no else clause).
let result = available_skills.value.map((skill) => {
if (
user.value.skills
.map((sk) => {
return sk.name;
})
.includes(skill.label)
) {
skill.active = false;
}
// ❌ no else to set `skill.active`
return skill;
});
However, your computed prop has a side effect of mutating the original data (i.e., in skill.active = false), which should be avoided. The mapping above should clone the original skill item, and insert a new active property:
const skills = user.value.skills.map(sk => sk.name);
let result = available_skills.value.map((skill) => {
return {
...skill,
active: skills.includes(skill.label)
}
});
demo

slice just returns a copy of the changed array, it doesn't change the original instance..hence computed property is not reactive
Try using below code
user.skills = user.skills.splice(index, 1);

Related

Why is my companion object being updated along with my target?

I have set up the following Reactive:
let blank = {
id: "new",
label: "",
details: "",
status: "",
due_date: "",
deadline: "",
enthusiasm: '0',
allotted: 30,
}
const state = reactive({
tray: "default",
active: null,
task: {
current: blank,
proposed: blank
}
})
and in one of my components, I am adding data like so:
setup() {
const store = inject('store')
async function refresh() {
if (store.state.active !== null) {
const response = await store.methods.loadTaskData(store.state.active)
store.state.task.current = response.data.results
store.state.task.proposed = response.data.results
}
}
watch(() => store.state.active, () => {
refresh()
})
refresh()
return {store, ...toRefs(store.state.task)}
}
With this, when I use v-model to update fields in the proposed object, for some reason it also updates the corresponding fields in the current object as well.
<input
v-model="proposed.label"
type="text"
class="form-field"
>
// Updates both "current" and "proposed" objects.
However, if I remove this line:
store.state.task.current = response.data.results
thereby leaving the "current" object blank, then everything works fine. Changes made to proposed aren't reflected in current. So how do I add response.data.results to both the current and proposed objects without having the wires get crossed like this?
Both current and proposed are being initialized as the same object. Instead, assign a copy of blank...
task: {
current: { ...blank },
proposed: { ...blank }
}

Set data field from getter and add extra computed field

I wanted to set fields inside data using getters:
export default {
data () {
return {
medications: [],
}
},
computed: {
...mapGetters([
'allMedications',
'getResidentsById',
]),
I wanted to set medications = allMedications, I know that we can user {{allMedications}} but my problem is suppose I have :
medications {
name: '',
resident: '', this contains id
.......
}
Now I wanted to call getResidentsById and set an extra field on medications as :
medications {
name: '',
resident: '', this contains id
residentName:'' add an extra computed field
.......
}
I have done this way :
watch: {
allMedications() {
// this.medications = this.allMedications
const medicationArray = this.allMedications
this.medications = medicationArray.map(medication =>
({
...medication,
residentName: this.getResidentName(medication.resident)
})
);
},
},
method: {
getResidentName(id) {
const resident = this.getResidentsById(id)
return resident && resident.fullName
},
}
But this seems problem because only when there is change in the allMedications then method on watch gets active and residentName is set.
In situations like this you'll want the watcher to be run as soon as the component is created. You could move the logic within a method, and then call it from both the watcher and the created hook, but there is a simpler way.
You can use the long-hand version of the watcher in order to pass the immediate: true option. That will make it run instantly as soon as the computed property is resolved.
watch: {
allMedications: {
handler: function (val) {
this.medications = val.map(medication => ({
...medication,
residentName: this.getResidentName(medication.resident)
});
},
immediate: true
}
}

VUEJS Can’t use api response data in the template

I need to populate a table using an array of objects got by an api call (axios).
This part is working fine.
In the store module (activity.js) I declared the array:
currentUserActivities: [],
In the mutations:
SET_CURRENT_USER_ACTIVITIES: (state, currentUserActivities) => {
state.currentUserActivities = currentUserActivities
},
In the actions:
setCurrentUserActivities({ commit }, userId) {
return new Promise((resolve, reject) => {
getUserActivities(userId).then(response => {
const currentUserActivities = response.results
commit('SET_CURRENT_USER_ACTIVITIES', currentUserActivities)
console.log('response current user activities: ', response.results)
resolve()
}).catch(error => {
console.log('Error setting single user activities: ', error)
reject(error)
})
})
},
Then I saved it in the getters module as so:
currentUserActivities: state => state.activity.currentUserActivities,
In the vue page, the relevant part of the script:
data() {
return {
currentUser: {},
userId: {
type: Number,
default: function() {
return {}
}
},
currentUserActivities: [],
}
},
mounted() {
const userId = this.$route.params.userId
this.$store.dispatch('user/setCurrentProfile', userId).then(() => {
const currentUser = this.$store.getters.currentProfile.user
this.currentUser = currentUser
console.log('user mounted user', currentUser)
this.$store.dispatch('activity/setCurrentUserActivities', userId).then(() => {
const currentUserActivities = this.$store.getters.currentUserActivities
console.log('activities on mounted', currentUserActivities)
})
})
},
In the template part, as I said, I will have a table data. Let's forget about it for now, I am just trying to get the array displayed raw, as so:
<div>
<p v-if="currentUserActivities.length = 0">
This user has no activities yet.
</p>
<p>CURRENT ACTIVITIES: {{ currentUserActivities }}</p>
<p>CURRENT USER: {{ currentUser }}</p>
</div>
The current user is displaying fine, in the browser I can see:
CURRENT USER: { "id": 1, "last_login": "20/09/2019 09:42:15", "is_superuser": false, "username": "admin", "first_name": "System", "last_name": "Dev", "email": "systems#dev.it", "is_staff": true, "is_active": false, "date_joined": "30/08/2019 09:03:40" }
The current user activities array, instead:
CURRENT ACTIVITIES: []
In the console I have both, leaving the user which is fine, the current user activities array is:
activities on mounted:
0: {...}
1: {…}
2:
activity: (...)
arrival_point: "SRID=4326;POINT (0 0)"
burns_calories: false
co2: "0.00"
co2_production: (...)
cost: (...)
created: (...)
default_cost: (...)
end: (...)
ecc. It's there, we can see it.
Inside the mounted, if we compare the code written for the user and the activities, the only difference is that I didn't set
this.currentUserActivities = currentUserActivities
If I do that, I loose the data in the console too (on the screen it remains empty array).
In the console I would have:
activities on mounted: (5) [{…}, {…}, {…}, {…}, {…}, __ob__: Observer]
1. length: 0
2. __ob__: Observer {value: Array(0), dep: Dep, vmCount: 0}
3. __proto__: Array
Also, even if I set
v-if="currentUserActivities.length = 0"
to display a p tag in case the array is really empty, it doesn't get displayed. This too is not right. I don't know if they can be related.
I tried many many subtle different versions of code, but none of them worked.
I know I am missing something (code is never wrong....) ....
Can someone enlighten me, please?
Thanks a lot.
x
First up, this:
this.$store.dispatch('activity/setCurrentUserActivities', userId).then(() => {
const currentUserActivities = this.$store.getters.currentUserActivities
console.log('activities on mounted', currentUserActivities)
})
As you've noted in the question, you aren't assigning currentUserActivities to anything. It should be this:
this.$store.dispatch('activity/setCurrentUserActivities', userId).then(() => {
const currentUserActivities = this.$store.getters.currentUserActivities
this.currentUserActivities = currentUserActivities
console.log('activities on mounted', currentUserActivities)
})
I know you mentioned that this didn't work in the question but it is required to get it working. It isn't sufficient, but it is necessary.
The reason the array appears empty is because of this:
v-if="currentUserActivities.length = 0"
Note that you are setting the length to 0, not comparing it to 0. It should be:
v-if="currentUserActivities.length === 0"
You've got some other problems too, though they're not directly related to the empty array.
Generally you shouldn't have data values for state in the store (unless you're taking copies for editing purposes, which you don't seem to be). Instead they should be exposed as computed properties, e.g.:
computed: {
currentUser () {
return this.$store.getters.currentProfile.user
}
}
Vuex includes a helper called mapGetters that can be used to shorten this a little, see https://vuex.vuejs.org/api/#component-binding-helpers, though some people prefer the explicitness of the longer form.
This is also a little strange:
return new Promise((resolve, reject) => {
getUserActivities(userId).then(response => {
Generally creating a new promise is regarded as a code smell as it is very rarely necessary. In this case you should probably just be returning the promise returned by getUserActivities instead. e.g.:
return getUserActivities(userId).then(response => {
Obviously you'd need to make other adjustments to accommodate the resolve and reject functions no longer being available. Instead of resolve you'd just return the relevant value (though there doesn't seem to be one in your case) and for reject you'd just throw the error instead.
I also notice that userId in your data is being assigned a type and default. Note that this is prop syntax and isn't valid for data properties. It isn't an error but the userId will just be equal to that whole object, it won't treat it as a configuration object.

Binding an object from checkboxes

I need to bind an object from checkboxes, and in this example, a checkbox is its own component:
<input type="checkbox" :value="option.id" v-model="computedChecked">
Here's my data and computed:
data() {
return {
id: 1,
title: 'test title',
checked: {
'users': {
},
},
}
},
computed: {
computedChecked: {
get () {
return this.checked['users'][what here ??];
},
set (value) {
this.checked['users'][value] = {
'id': this.id,
'title': this.title,
}
}
},
....
The above example is a little rough, but it should show you the idea of what I am trying to achieve:
Check checkbox, assign an object to its binding.
Uncheck and binding is gone.
I can't seem to get the binding to worth though.
I assume you want computedChecked to act like an Array, because if it is a Boolean set, it will receive true / false on check / uncheck of the checkbox, and it should be easy to handle the change.
When v-model of a checkbox input is an array, Vue.js expects the array values to stay in sync with the checked status, and on check / uncheck it will assign a fresh array copy of the current checked values, iff:
The current model array contains the target value, and it's unchecked in the event
The current model array does not contain the target value, and it's checked in the event
So in order for your example to work, you need to set up your setter so that every time the check status changes, we can get the latest state from the getter.
Here's a reference implementation:
export default {
name: 'CheckBoxExample',
data () {
return {
id: 1,
title: 'test title',
checked: {
users: {}
}
}
},
computed: {
computedChecked: {
get () {
return Object.getOwnPropertyNames(this.checked.users).filter(p => !/^__/.test(p))
},
set (value) {
let current = Object.getOwnPropertyNames(this.checked.users).filter(p => !/^__/.test(p))
// calculate the difference
let toAdd = []
let toRemove = []
for (let name of value) {
if (current.indexOf(name) < 0) {
toAdd.push(name)
}
}
for (let name of current) {
if (value.indexOf(name) < 0) {
toRemove.push(name)
}
}
for (let name of toRemove) {
var obj = Object.assign({}, this.checked.users)
delete obj[name]
// we need to update users otherwise the getter won't react on the change
this.checked.users = obj
}
for (let name of toAdd) {
// update the users so that getter will react on the change
this.checked.users = Object.assign({}, this.checked.users, {
[name]: {
'id': this.id,
'title': this.title
}
})
}
console.log('current', current, 'value', value, 'add', toAdd, 'remove', toRemove, 'model', this.checked.users)
}
}
}
}

MobX - Select single item in array, unselect all others?

I have the following observable array of search engines.
#observable favoriteSearchEngine = [
{ 'provider' : 'google', 'selected': true },
{ 'provider' : 'yahoo', 'selected': false },
{ 'provider' : 'bing', 'selected': false },
];
The user should only be able to select one at a time from the UI. So if they choose yahoo for example, yahoo would get selected: true and any other provider would get selected: false
This action handles the click:
#action onClickFavoriteSearchEngine = (provider) => {
alert(provider); // yahoo shows here
// How to do this step, only selected provider true and falsify all others in the array?
}
The solution given by #mweststrate works great, but since you are using an action (which also is a transaction), you could just unselect the previously selected, and select the new one if you would prefer:
#action onClickFavoriteSearchEngine = (provider) => {
alert(provider); // yahoo shows here
favoriteSearchEngine.forEach(e => e.selected = false);
favoriteSearchEngine.find(e => e.provider === provider).selected = true;
}
I would introduce a single observable representing selection, and derive the selected state from that:
#observable selection = null
#observable favoriteSearchEngine = [
{ 'provider' : 'google', 'selected': function() {
return selection === this
}
]
If you now assign another engine to the selection a few times, you will see that the selected state of the engines will update accordingly
(N.B. don't use arrow functions if declaring a plain object + derivation like this, to avoid issues with this)