Why is my companion object being updated along with my target? - vue.js

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 }
}

Related

VueJS/vuex application design question - how to initialize local data with getters

Context:
I have a reports application that contains a report editor. This Report Editor is used to edit the contents of the report, such as the title, the criteria for filtering the results, the time range of results, etc..
The Problem:
There is something wrong with the way I have used Vuex/Vuejs in my components I believe. My store contains getters for each aspect of this report editor. Like this:
const getters = {
activeReportTitle: state => {
return state.activeReport.title;
},
activeReportID: state => {
return state.activeReport.id;
},
timeframe: state => {
return state.activeReport.timeframe;
},
includePreviousData: state => {
return state.activeReport.includePreviousData;
},
reportCriteria: state => {
return state.activeReport.reportCriteria;
},
emailableList: state => {
return state.activeReport.emailableList;
},
dataPoints: state => {
return state.activeReport.configuration?.dataPoints;
},
...
Each getter is used in a separate component. This component uses the getter only to initialize the local data, and uses actions to modify the state. The way I have done this is by adding a local data property and a watcher on the getter that changes the local data property. The component is using the local data property and that data property is sent to the action and the getter is updated.
ReportSearchCriteria.vue
...
data() {
return {
localReportCriteria: [],
currentCriteria: "",
};
},
watch: {
reportCriteria: {
immediate: true,
handler(val) {
this.localReportCriteria = [...val];
}
}
},
computed:{
...reportStore.mapGetters(['reportCriteria'])
},
methods: {
...reportStore.mapActions(["updateReportCriteria"]),
addSearchCriteria() {
if (this.currentCriteria) {
this.localReportCriteria.push(this.currentCriteria);
this.updateReportCqriteria(this.localReportCriteria);
}
this.currentCriteria = "";
this.$refs['reportCriteriaField'].reset();
},
...
The hierarchy of the components is set up like this
Reports.Vue
GraphEditor.vue
ReportSearchCriteria.vue
Could you clarify what the problem is? Does the 'reportCriteria' not get updated when it's supposed to? How does the function 'updatedReportCriteria' look like? You use mutations to update a state in the store. Also, you have a typo when you're calling the action.

Prevent Vue Multiple Select to Store an Empty Array

I want this select multiple to pre-select one option, and not be able to deselect all options.
Whenever the last selected option is deselected it should be reselected. In other words when the user tries to deselect the last selected option it should visually not be deselected.
<template>
<b-select
if="Object.keys(doc).length !== 0 /* wait until firebase has loaded */"
:options="computedOptions"
v-model="model"
multiple
#input="onChange"
/>
</template>
<script>
//import Vue from 'vue'
import { fb } from "../fbconf";
export default {
name: "MyMultiSelect",
props: {
doc: Object, // firestore document
},
data() {
return {
options: []
};
},
firestore() {
var options = fb.db.collection("options");
return {
options: options
};
},
computed: {
computedOptions: function() {
return this.options.map(function(option) {
return {
text: option.name,
value: option.id
};
});
},
// to make sure mySelectedOptions is an array, before this.doc is loaded
// I use the following custom model
// because not using 'get' below causes a warning:
// [Vue warn]: <select multiple v-model="localValue"> expects an Array value for its binding, but got Undefined
model: {
get: function() {
if (!this.doc.hasOwnProperty('mySelectedOptions')) return []; // empty array before this.doc is loaded
else return this.doc['mySelectedOptions'];
},
set: function(newValue) {
// here I can prevent the empty array from being stored
// but visually the user can deselect all options, which is bad UX
//if (Array.isArray(newValue) && newValue.length > 0) this.doc['mySelectedOptions'] = newValue;
}
},
},
methods: {
onChange: function(newValue){
// I can manually store the array as I want here
// but I cannot in any way prevent the user from deselecting all options
if (Array.isArray(newValue) && newValue.length > 0) this.doc['mySelectedOptions'] = newValue;
else {
// none of these reselects the last selected option
var oldValue = this.doc['mySelectedOptions'];
this.doc['mySelectedOptions'] = this.doc['mySelectedOptions'];
//this.$forceUpdate();
//this.$emit("change", newValue);
//Vue.set(this.doc, 'mySelectedOptions', this.doc['mySelectedOptions']);
}
}
}
};
</script>
You could add watcher and when length becomes 0 just add previous value.
watch: {
model(val, oldVal) {
if(val.length == 0 && oldVal.length > 0) {
// take only one item in case there's clear button or etc.
this.model = [oldval[0]];
}
}
}

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
}
}

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)
}
}
}
}

Vue.js 2: action upon state variable change

I am using a simple state manager (NOT vuex) as detailed in the official docs. Simplified, it looks like this:
export const stateholder = {
state: {
teams: [{id: 1, name:'Dallas Cowboys'}, {id: 2, name:'Chicago Bears'}, {id: 3, name:'Philadelphia Eagles'}, {id:4, name:'L.A. Rams'}],
selectedTeam: 2,
players: []
}
getPlayerList: async function() {
await axios.get(`http://www.someapi.com/api/teams/${selectedTeam}/players`)
.then((response) => {
this.state.players = response.data;
})
}
}
How can I (reactively, not via the onChange event of an HTML element) ensure players gets updated (via getPlayerList) every time the selectedTeam changes?
Any examples of simple state that goes a little further than the official docs? Thank you.
Internally, Vue uses Object.defineProperty to convert properties to getter/setter pairs to make them reactive. This is mentioned in the docs at https://v2.vuejs.org/v2/guide/reactivity.html#How-Changes-Are-Tracked:
When you pass a plain JavaScript object to a Vue instance as its data
option, Vue will walk through all of its properties and convert them
to getter/setters using Object.defineProperty.
You can see how this is set up in the Vue source code here: https://github.com/vuejs/vue/blob/79cabadeace0e01fb63aa9f220f41193c0ca93af/src/core/observer/index.js#L134.
You could do the same to trigger getPlayerList when selectedTeam changes:
function defineReactive(obj, key) {
let val = obj[key]
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
return val;
},
set: function reactiveSetter(newVal) {
val = newVal;
stateholder.getPlayerList();
}
})
}
defineReactive(stateholder.state, 'selectedTeam');
Or you could set it up implicitly using an internal property:
const stateholder = {
state: {
teams: [/* ... */],
_selectedTeam: 2,
get selectedTeam() {
return this._selectedTeam;
},
set selectedTeam(val) {
this._selectedTeam = val;
stateholder.getPlayerList();
},
players: []
},
getPlayerList: async function() {
/* ... */
},
};
Your question is also similar to Call a function when a property gets set on an object, and you may find some more information there.
You could use v-on:change or #change for short to trigger getPlayerList.
Here a fiddle, simulating the request with setTimeout.