I have to pull in data from 12 specific departments. When my Vue store gets mounted, I fire off some actions to load in these orders via 12 different ajax calls. (These calls are to different API end points and servers).
Once I get a response I commit them to the state. So in my state I would have the following data set to arrays:
state: {
sportsOrders: [],
photographicOrders: []
//And more..
},
Now I need to filter these specific orders based on if they are late, in production, etc. So I have a getter that is setup like so:
getters: {
getDepartmentLateOrders: (state) => (department) => {
let resultArray = []
state.department.forEach(function(order, index) {
let now = new Date()
let orderShipDate = new Date(order.ExpectedShipDate)
let diff = date.getDateDiff(orderShipDate, now)
if(diff < 0) {
resultArray.push(order)
}
})
return resultArray
},
}
And then when I need to use it in a component...
return this.$store.getters.getDepartmentLateOrders(this.department+'Orders')
//for example it will end up passing 'sportsOrders' to the getter
However, when I do this, Vue doesn't use the state data for sportsOrders so I know I'm not accessing it correctly. It says:
vue.runtime.esm.js?ff9b:574 [Vue warn]: Error in render: "TypeError: Cannot read property 'forEach' of undefined"
If I hardcode my getter like so it works as expected though (but I don't want to do this for 12 departments)..
state.sportsOrders.forEach(function(order, index) {
...
}
How can I set this up so I can use this one getter and have it work with all 12 departments? I only want to pass the name of the department if possible.
Related
I'm developing a helpdesk tool in which I have a kanban view.
I previously used nested serializers in my backend and I managed to have everything working with a single query but it's not scalable (and it was ugly) so I switched to another schema :
I query my helpdesk team ('test' in the screenshot)
I query the stages of that team ('new', 'in progress')
I query tickets for each stage in stages
So when I mount my component, I do the following :
async mounted () {
if (this.helpdeskTeamId) {
await this.getTeam(this.helpdeskTeamId)
if (this.team) {
await this.getTeamStages(this.helpdeskTeamId)
if (this.stages) {
for (let stage of this.stages) {
await this.getStageTickets(stage)
}
}
}
}
},
where getTeam, getTeamStages and getStageTickets are :
async getTeam (teamId) {
this.team = await HelpdeskTeamService.getTeam(teamId)
},
async getTeamStages (teamId) {
this.stages = await HelpdeskTeamService.getTeamStages(teamId)
for (let stage of this.stages) {
this.$set(stage, 'tickets', [])
}
},
async getStageTickets (stage) {
const tickets = await HelpdeskTeamService.getTeamStageTickets(this.helpdeskTeamId, stage.id)
// tried many things here below but nothing worked.
// stage.tickets = stage.tickets.splice(0, 0, tickets)
// Even if I try to only put one :
// this.$set(this.stages[this.stages.indexOf(stage)].tickets, 0, tickets[0])
// I see it in the data but It doesn't appear in the view...
// Even replacing the whole stage with its tickets :
// stage.tickets = tickets
// this.stages.splice(this.stages.indexOf(stage), 1, stage)
},
In getTeamStages I add an attribute 'tickets' to every stage to an empty list. The problem is when I query all the tickets for every stage. I know how to insert a single object in an array with splice or how to delete one object from an array but I don't know how to assign a whole array to an attribute of an object that is in an array while triggering the Vue reactivity. Here I'd like to put all the tickets (which is a list), to stage.tickets.
Is it possible to achieve this ?
If not, what is the correct design to achieve something similar ?
Thanks in advance !
EDIT:
It turns out that there was an error generated by the template part. I didn't think it was the root cause since a part of the view was rendered. I thought that it would have prevent the whole view from being rendered if it was the case. But finally, in my template I had a part doing stage.tickets.length which was working when using a single query to populate my view. When making my API more granular and querying tickets independently from stages, there is a moment when stage has no tickets attribute until I set it manually with this.$set(stage, 'tickets', []). Because of that, the template stops rendering and raises an issue. But the ways of updating my stage.tickets would have worked without that template issue.
I could update the stages reactively. Here is my full code; I used the push method of an array object and it works:
<template>
<div>
<li v-for="item in stages" :key="item.stageId">
{{ item }}
</li>
</div>
</template>
<script>
export default {
data() {
return {
stages: [],
};
},
methods: {
async getTeamStages() {
this.stages = [{ stageId: 1 }, { stageId: 2 }];
for (let stage of this.stages) {
this.$set(stage, "tickets", []);
}
for (let stage of this.stages) {
await this.getStageTickets(stage);
}
},
async getStageTickets(stage) {
const tickets = ["a", "b", "c"];
for (let ticket of tickets) {
this.stages[this.stages.indexOf(stage)].tickets.push(ticket);
}
},
},
mounted() {
this.getTeamStages();
},
};
</script>
It should be noted that I used the concat method of an array object and also works:
this.stages[this.stages.indexOf(stage)].tickets = this.stages[this.stages.indexOf(stage)].tickets.concat(tickets);
I tried your approaches some of them work correctly:
NOT WORKED
this.$set(this.stages[this.stages.indexOf(stage)].tickets, tickets)
WORKED
this.$set(this.stages[this.stages.indexOf(stage)].tickets, 0, tickets[0]);
WORKED
stage.tickets = tickets
this.stages.splice(this.stages.indexOf(stage), 1, stage)
I'm sure it is XY problem..
A possible solution would be to watch the selected team and load the values from there. You seem to be loading everything from the mounted() hook, and I suspect this won't actually load all the content on demand as you'd expect.
I managed to make it work here without needing to resort to $set magic, just the pure old traditional vue magic. Vue will notice the properties of new objects and automatically make then reactive, so if you assign to them later, everything will respond accordingly.
My setup was something like this (showing just the relevant parts) -- typing from memory here, beware of typos:
data(){
teams: [],
teamId: null,
team: null
},
watch:{
teamId(v){
this.refreshTeam(v)
}
},
methods: {
async refreshTeam(id){
let team = await fetchTeam(id)
if(!team) return
//here, vue will auomaticlly make this.team.stages reactive
this.team = {stages:[], ...team}
let stages = await fetchStages(team.id)
if(!stages) return
//since this.team.stages is reactive, vue will update reactivelly
//turning the {tickets} property of each stage reactive also
this.team.stages = stages.map(v => ({tickets:[], ...v}))
for(let stage of this.team.stages){
let tickets = await fetchTickets(stage.id)
if(!tickets) continue
//since tickets is reactive, vue will update it accordingly
stage.tickets = tickets
}
}
},
async mounted(){
this.teams = fetchTeams()
}
Notice that my 'fetchXXX' methods would just return the data retrieved from the server, without trying to actually set the component data
Edit: typos
I have the following component to quickly configure stops on a delivery/pickup route and how many items are picked up and dropped
and this is the data model, note the 2 is the one next to 'a' on the previous image.
If a click the + or - button, in the first item, it behaves as expected,
But second item doesn't work as expected
I've already checke a couple of posts on object property update likes this ones
Is it possible to mutate properties from an arbitrarily nested child component in vue.js without having a chain of events in the entire hierarchy?
https://forum.vuejs.org/t/nested-props-mutations-hell-internet-need-clarification/99346
https://forum.vuejs.org/t/is-mutating-object-props-bad-practice/17448
among others, and came up with this code:
ADD_ITEM_TO_SELECTED_STOP(state, payload) {
let count = state.selectedStop.categories[payload.catIndex].items[payload.itemIndex].count;
const selectedCat = state.selectedStop.categories[payload.catIndex];
const currentItem = selectedCat.items[payload.itemIndex];
currentItem.count = count + 1;
selectedCat.items[payload.itemIndex] = currentItem;
Vue.set(state.selectedStop.categories, payload.catIndex, selectedCat);
},
and as the button event:
addToItem(item) {
this.$store.dispatch("addItemToSelectedStop", {
catIndex: item.catIndex,
itemIndex: item.itemIndex
})
},
And finally my computed property code:
items() {
let finalArray = [];
this.selectedStop.categories.forEach(
(cat, catIndex) => {
let selected = cat.items.filter((item) => item.count > 0 );
if (selected.length > 0) {
//here we add the catIndex and itemIndex to have it calling the rigth shit
selected = selected.map(val => {
let itemIndex = cat.items.findIndex( itemToFind => itemToFind.id === val.id);
return {
...val,
catIndex: catIndex,
itemIndex: itemIndex,
}})
finalArray = finalArray.concat(selected);
}
});
return finalArray;
}
What confuses me the most is that I have almost the same code in another component, and there it's working as expected, and although the model is changed, the computed property is only recalculated on the first item,
After reading this gist and taking a look again at the posts describing this kind of issue, I decided to give it a try and just make a copy of the whole stored object not just the property, update it, then set it back on vuex using Vue.set, and that did the trick, everything is now working as expected, this is my final store method.
ADD_ITEM_TO_SELECTED_STOP(state, payload) {
let selectedLocalStop = JSON.parse(JSON.stringify(state.selectedStop));
let count = selectedLocalStop.categories[payload.catIndex].items[payload.itemIndex].count;
selectedLocalStop.categories[payload.catIndex].items[payload.itemIndex].count = count + 1;
Vue.set(state,"selectedStop", selectedLocalStop );
//Now we search for this step on the main list
const stepIndex = state.stops.findIndex(val => val.id === selectedLocalStop.id);
Vue.set(state.stops,stepIndex, selectedLocalStop );
},
I had to add the last bit after updating the whole object, because, originally, the array items were updated when the selected item was changed, I guess some sort of reference, but with the object creation, that relationship no longer works "automatic" so I need to update the array by hand
i've a reactive object, on the save function i call toRaw in order to remove de object reactivity, but, when i change the reactive object props, also the group object is changing....
How???
const newGroup = reactive<Group>({ id: undefined, name: '', enabled: false })
const handleSave = () => {
const group = toRaw(newGroup)
persist(group).then(() => {
newGroup.id = undefined
newGroup.name = 'demo'
console.log(isReactive(group)) //say false but the group.name value is demo
})
}
destructuring the reactive obj as const group = { ...newGroup } looks like it works, still no understand why toRaw is not working.
EDIT:
the whole problem comes from a sense of lack of control when dealing with reactive objects for example:
the following cumputed object retrieves a list from the store which is then drawn in the template as rows of a table at which click the selected element is changed of state. in the function I am forced to deconstruct records to avoid that the modification trigger the change of enabled before it is persisted
Is there a better way?
Should I make readonly groups?
//in setup()
const groups = computed <Group []> (() => getters ['registry / groups'])
const toggle = (record: Group) => {
const group = { ...record }
group.enabled = !group.enabled
persist(group)
}
//in template
<a-menu-item # click = "toggle (record)">
This is expected behaviour, but the description may not be detailed enough and cause some confusion.
according to toRaw docs
Returns the raw, original object of a reactive or readonly proxy. This is an escape hatch that can be used to temporarily read without incurring proxy access/tracking overhead or write without triggering changes. It is not recommended to hold a persistent reference to the original object. Use with caution.
What is happening when you use toRaw, is that you're getting back the original object that you passed into the reactive function without the Proxy. That means that the object is the same object (o and g in example 👇). toRaw only allows you to "escape" the reactivity/listeners bound to the object if you use toRaw instead of the Proxy itself. Even though the objects update, the reactivity (trickling down to DOM) is not applied.
Here is an example:
let o = {name:"Ja Ja"};
let m = Vue.reactive(o);
let g = Vue.toRaw(m);
console.log(o.name, m.name, g.name);
g.name = g.name + " Ding Dong";
console.log(o.name, m.name, g.name);
console.log("Is 'm' same as 'o'? ", m == g);
console.log("Is 'g' really just 'o'? ", o == g);
<script src="https://unpkg.com/vue#next/dist/vue.global.prod.js"></script>
If you want to create an object that will not update the original object, the simplest way is to clone it.
Most of the time you can use JSON.parse(JSON.stringify(o)). The caveat is that you get into issues if you have cyclical references (ie. {a:b, b:a}).
Another option is to use destructuring const copy = {...o}. The caveat here is that it is a shallow copy. Example o = {a:{b:"I will update"}, c:"I won't"}, where copy.a is still same object as o.a
That would then look like either
let o = {name:"Ja Ja"};
let m = Vue.reactive(o);
let g0 = {...Vue.toRaw(m)}; // destructure
let g1 = JSON.parse(JSON.stringify(Vue.toRaw(m))); // clone with json
Alternatively, you can also use some library that handles cloning object for you.
You have a misunderstanding of reactivity here. A reactive object connects the data with DOM, not between javascript objects, i.e. when a property of the reactive object is updated, the DOM is automatically updated.
toRaw returns the raw, original object of a reactive or readonly proxy, i.e group and newGroup are still the same object, the difference is that group would not trigger a DOM update as newGroup does.
Example, UI is updated when you update newGroup, but updating group won't trigger a DOM change. However the two objects still have the same reference, which is why group.name and newGroup.name are always the same.
Vue.createApp({
setup() {
const newGroup = Vue.reactive({ id: undefined, name: '', enabled: false })
const group = Vue.toRaw(newGroup)
return {
newGroup,
group
}
}
}).mount('#app')
<script src="https://unpkg.com/vue#next/dist/vue.global.prod.js"></script>
<div id="app">
<button #click="newGroup.name = 'reactive update'">reactive update</button>
newGroup Name: {{newGroup.name}}<br/>
<button #click="group.name = 'raw update'">raw update</button>
group Name: {{group.name}}
</div>
it is normal that isReactive(group) return false.
Actually in your code we have const group = toRaw(newGroup)
=> group is the original object of the reactive value newGroup.
=> group is a raw value, not a reactive value
if you tried isReactive(newGroup) this will return true
My solution is based the article in:
https://chrysanthos.xyz/article/how-to-get-the-data-of-a-proxy-object-in-vue-js-3/
1 - First import { toRaw } from "vue";
2 - In my vuex on return getters:
`
// getters
const getters = {
listOperations:(state: { operations: any; }) => {
return toRaw(state.operations);
}
}
`
3 - Browser: (9) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
I recently built a small application with Vue.js and Express.js. There are few components needed to be prepared by the servers, e.g., the combobox for Article.Category and Article.User. The options for these 2 components needed to be rendered from server. I use <component /> as the placeholder for these 2 components in the article edit form:
<component v-bind:is="user_selection_component"></component>
<component v-bind:is="category_selection_component"></component>
I use the template string for initialising the components, the template string result.data.template is passed by server:
let org_data = original_store;
let new_data = () => {
org_data['remote_options'] = result.data.remote_options;
//if there is any default value, then assign the value to field referred by "model_name"
if(model_name && result.data.preset_value){
let previous_value = $shared.index(org_data, model_name);
if(!previous_value){
$shared.index(org_data, model_name, result.data.preset_value);
}
}
return org_data;
}
var default_cb = ()=>{console.info('['+model_name+'].default_cb()')};
let TempComponent = {
template: result.data.template,
methods: component_ref.methods,
data: new_data,
mounted: cb !== null ? cb.bind(this, org_data) : default_cb.bind(this)
};
app[mount_component] = TempComponent;
Here is the problem, the data method returns a new Observable store for the dynamically loaded components, they don't share the same store object with the parent component, which is the article edit from. Hence, if I want to modify the category field value or user field value, I have to let the callback function cb to accept the store objects of these 2 dynamically loaded components. Otherwise, from the parent component, I could not modify the values in these 2 components.
So I came up with a temporary workaround, I passed the setter method as the callback function to these dynamically loaded functions:
let set_user_id = null;
let set_cate_id = null;
(org_store) => { set_user_id = (new_id) => { org_store.form.user_id = new_id; }}
(org_store) => { set_cate_id = (new_id) => {org_store.form.category_id = new_id; }}
After I load other components or anytime I want to set the category/user value, I can just call set_user_id($new_user_id) or set_cate_id($new_category_id);
I don't like this work around at all. I tried to use the event handler to emit the new values into these 2 components. But I couldn't access these 2 dynamically loaded component via $ref. Is there a better way to let data be shared between dynamically loaded components? Thanks.
If your components will accept props, you can localize your event bus, which is a little nicer than having a global. The parent component creates the bus as a data item:
data() {
...
bus: new Vue()
}
The components accept it as a prop:
<component v-bind:is="user_selection_component" :bus="bus"></component>
<component v-bind:is="category_selection_component" :bus="bus"></component>
and you use it as in your answer, except referring to this.bus instead of just bus.
I don't think what I have now is the best solution. After consulting with other people, I took event bus as the better solution. So I modified my code as:
In my init component:
let org_data = original_store;
let new_data = () => {
org_data['remote_options'] = result.data.remote_options;
//if there is any default value, then assign the value to field referred by "model_name"
if(model_name && result.data.preset_value){
let previous_value = $shared.index(org_data, model_name);
if(!previous_value){
$shared.index(org_data, model_name, result.data.preset_value);
}
}
return org_data;
}
var default_cb = () => { console.info('['+model_name+'].default_cb()') };
let TempComponent = {
template: result.data.template,
methods: component_ref.methods,
data: new_data,
mounted: cb !== null ? cb.bind(this, org_data) : default_cb.bind(this)
};
bus.$on('set.' + model_name, function(value){
$shared.index(org_data, model_name, value);
});
The difference is here:
bus.$on('set.' + model_name, function(value){
$shared.index(org_data, model_name, value);
});
The bus is the common event bus created by Vue():
let bus = new Vue()
From the parent component, I can just use this event bus to emit the event:
bus.$emit('set.form.user_id', this.form.user_id);
I do feel better after changing to this solution. But I still appreciate if there is an even better way. Thanks.
i want to check the returned value of $http.get() but i get undefined value. Here is my vue js code:
var vm = new Vue({
el: '#permissionMgt',
data: {
permissionID: []
},
methods:{
fetchPermissionDetail: function (id) {
this.$http.get('../api/getComplaintPermission/' + id, function (data) {
this.permissionID = data.permissionID; //permissionID is a data field of the database
alert(this.permissionID); //*****this alert is giving me undefined value
});
},
}
});
Can you tell me thats the problem here?.. btw $http.get() is properly fetching all the data.
You need to check what type is the data returned from the server. If #Raj's solution didn't resolve your issue then probably permissionID is not present in the data that is returned.
Do a colsole.log(data) and check whether data is an object or an array of objects.
Cheers!
Its a common js error. Make the following changes and it will work.
fetchPermissionDetail: function (id) {
var self = this; //add this line
this.$http.get('../api/getComplaintPermission/' + id, function (data) {
self.permissionID = data.permissionID; //replace "this" with "self"
});
},
}
the reason is this points to window inside the anonymous function function()
This is a well known js problem. If you are using es2015 you can use arrow syntax (() => { /*code*/ }) syntax which sets this correctly