I've been stumped on getting my data to be reactive down to some chartjs charts.
I have a vuex store that when the app is mounted fires off an action to do some API cals (10 in total). Each API call is related to a department and it returns an array of all orders. That all works fine and I can see the data in vuex in the dev tools.
On my home page, I attempt to build 10 different charts. each chart has datasets related to a department's orders. The datasets are broken down to display late orders, on time, already shipped, etc.
So in my component that passes the data to the charts I use a state getter to fill out my data sets. So one of my getters is like this:
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)
let statusCheck = outOfProductionStatuses
if(diff < 0 && !order.Invoiced && !statusCheck.includes(order.LastStatus)) {
resultArray.push(order)
}
})
return resultArray
Here is my store :
const store = new Vuex.Store({
state: {
//These are filled from setOrdersData()
dept1Orders: [],
dept2Orders: [],
dept3Orders: [],
loaded: false,
departmentsArray : ['dept1', 'dept2', 'dept3']
},
actions: {
//This is called when the app is mounted
loadOrders: function({ commit }) {
let departmentSeach = this.state.departmentsArray
departmentSeach.forEach(function(dept, index) {
api.getDeptStatus(dept.toLowerCase()).then((response) => {
store.commit('setOrdersData', { dept: dept, orders: response })
}, (err) => {
console.log(err)
})
})
this.state.loaded = true
}
},
mutations: {
//For reference: https://vuex.vuejs.org/en/mutations.html
setOrdersData: (state, payload) => {
Vue.set(state, payload.dept+'Orders', payload.orders)
},
},
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)
let statusCheck = outOfProductionStatuses
if(diff < 0 && !order.Invoiced && !statusCheck.includes(order.LastStatus)) {
resultArray.push(order)
}
})
return resultArray
},
getDepartmentShipTodayOrders: (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)
let statusCheck = outOfProductionStatuses
if(diff === 0 && !order.Invoiced && !statusCheck.includes(order.LastStatus)) { //TODO: Have this not exclude the out of production orders. But maybe have it highlight so it knows?
resultArray.push(order)
}
})
return resultArray
},
getDepartmentOutOfProductionOrders: (state) => (department) => {
let resultArray = []
let statusCheck = outOfProductionStatuses
state[department].forEach(function(order, index) {
if(statusCheck.includes(order.LastStatus) && !order.Invoiced) {
resultArray.push(order)
}
})
return resultArray
},
getDepartmentShippedOrders: (state) => (department) => {
let resultArray = []
//let statusCheck = ['SHIPPING-3']
state[department].forEach(function(order, index) {
if(order.Invoiced) {
resultArray.push(order)
}
})
return resultArray
},
getDepartmentOnTimeOrders: (state) => (department) => {
let resultArray = []
//let statusCheck = ['SHIPPING-3']
state[department].forEach(function(order, index) {
let now = new Date()
let orderShipDate = new Date(order.ExpectedShipDate)
let diff = date.getDateDiff(orderShipDate, now)
let statusCheck = outOfProductionStatuses
if(diff > 0 && !order.Invoiced && !statusCheck.includes(order.LastStatus)) {
resultArray.push(order)
}
})
return resultArray
},
}
})
Here is how I get to the data in the component that needs it:
fillData () {
let store = this.$store;
let departmentSeach = this.departmentArray
let that = this
departmentSeach.forEach(function(dept, index) {
console.log('get '+ dept)
that[dept+'DataCollection'] = Object.assign({}, that[dept+'DataCollection'],
{
datasets: [{
data: [
store.getters.getDepartmentShipTodayOrders(dept+'Orders').length,
store.getters.getDepartmentLateOrders(dept+'Orders').length,
store.getters.getDepartmentOutOfProductionOrders(dept+'Orders').length,
store.getters.getDepartmentShippedOrders(dept+'Orders').length,
store.getters.getDepartmentOnTimeOrders(dept+'Orders').length
//12, 13, 14, 15, 16
],
backgroundColor: that.defaultColors
}],
// These labels appear in the legend and in the tooltips when hovering different arcs
labels: that.defaultLables
})
})
this.loadingVisible = false
this.loaded = true
},
The problem is that my getters are running BEFORE all my API requests are completed. I had thought that this would be fine because it would be reactive, so as data came in and set the state data, the component that is using the getter would have data bound to it because I'm referencing the state getters that are using the state data.
On my initial charts page load, the charts don't render with the data, except the very first department (probably because the API call is fast enough). If I view a page in the app and go back to the charts page all my data is in there for the other departments and it works great.
So my question... what am I doing wrong in this example that the data is not being reactive? I'm sure it's some kind of reactivity gotcha that I am overlooking.
So the main issue that this all was related to was how I instantiated my data for the charts. I set the empty department order objects in data. However, the correct way of doing it is to use computed instead because the data is derived from the store and will be reactive to the data as it comes in to my stores and the getters respond. After I did that everything worked just fine.
Related
I would like to reorder arrays when using mobx state tree.
Say I have this example taken from the example page.
How do I get to reorder my ToDos in the TodoStore.
As a simplified example, say my todos are ['todo1, todo2'], how do I change them so that the new array is ['todo2, todo1']?
const Todo = types
.model({
text: types.string,
completed: false,
id: types.identifierNumber
})
.actions((self) => ({
remove() {
getRoot(self).removeTodo(self)
},
edit(text) {
if (!text.length) self.remove()
else self.text = text
},
toggle() {
self.completed = !self.completed
}
}))
const TodoStore = types
.model({
todos: types.array(Todo),
filter: types.optional(filterType, SHOW_ALL)
})
.views((self) => ({
get completedCount() {
return self.todos.filter((todo) => todo.completed).length
},
}))
.actions((self) => ({
addTodo(text) {
const id = self.todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1
self.todos.unshift({ id, text })
},
removeTodo(todo) {
destroy(todo)
},
}))
export default TodoStore
Thanks a lot!
If you want move the second todo to the first index in the array you could create a new action and splice the second todo out and then unshift it back in:
swapFirstTwoTodos() {
const secondTodo = self.todos.splice(1, 1)[0];
self.todos.unshift(secondTodo);
}
I have following store defined:
state: () => ({
infoPackCreationData: null,
infoPackCreationTab: null,
}),
getters: {
infoPackImage(state: any) {
return state.infoPackCreationTab && state.infoPackCreationTab.infopackContents
? state.infoPackCreationTab.infopackContents.filter((item: any) => item.type === "IMAGE")
: [];
}
},
mutations: {
setImageData(state:any, infopackImageData: any) {
state.infoPackCreationTab.infopackContents.filter((item: any) => {if(item.type === "IMAGE")
item = infopackImageData
console.log(item , 'this is items');
return item})
}
},
actions: {
setImageData(context: any, payload: any) {
context.commit('setImageData', payload)
}
}
and in my component I am using the computed to get the imageList:
computed: {
...mapGetters("creationStore", ["infoPackImage"]),
imageList: {
get() {
return this.infoPackImage ?? [];
},
set(value) {
this.$store.dispatch('creationStore/setImageData', value);
}
}
},
The problem is I want to edit a value of the imageList by index using draggable libarary,
but imageList does not act reactive and it just move the image and not showing the other image in the previous index:
async imageChange(e) {
this.loading = true
let newIndex = e.moved.newIndex;
let prevOrder = this.imageList[newIndex - 1]?.order ?? 0
let nextOrder = this.imageList[newIndex + 1]?.order ?? 0
const changeImageOrder = new InfopackImageService();
try {
return await changeImageOrder.putImageApi(this.$route.params.infopackId,
this.$route.params.tabId,
e.moved.element.id, {
title: e.moved.element.title,
infopackAssetRef: e.moved.element.infopackAssetRef,
order: nextOrder,
previousOrder: prevOrder,
}).then((res) => {
let image = {}
let infopackAsset = e.moved.element.infopackAsset
image = {...res, infopackAsset};
Vue.set(this.imageList, newIndex , image)
this.loading = false
return this.imageList
});
} catch (e) {
console.log(e, 'this is put error for tab change')
}
},
Array.prototype.filter doesn't modify an array in-place, it returns a new array. So this mutation isn't ever changing any state:
mutations: {
setImageData(state:any, infopackImageData: any) {
state.infoPackCreationTab.infopackContents.filter((item: any) => {if(item.type === "IMAGE")
item = infopackImageData
console.log(item , 'this is items');
return item})
}
},
So, if you intend to change state.infoPackCreationTab.infopackContents, you'll need to assign the result of filter():
mutations: {
setImageData(state:any, infopackImageData: any) {
state.infoPackCreationTab.infopackContents = state.infoPackCreationTab.infopackContents.filter(...)
However, since state.infoPackCreationTab did not have an infopackContents property during initialization, it will not be reactive unless you use Vue.set() or just replace the whole infoPackCreationTab object with a new one (see: Vuex on reactive mutations):
mutations: {
setImageData(state:any, infopackImageData: any) {
state.infoPackCreationTab = {
...state.infoPackCreationTab,
infopackContents: state.infoPackCreationTab.infopackContents.filter(...)
};
I'm learning vuejs and I'm doing a weather app, the goal is to rank cities with an index (humidex). I fetch weather information by API (axios) in order to collect data from several cities. I want to auto update data every x minutes, problem : some of my results are duplicated (the new data don't replace the old one).
I tried to set an unique key (based on latitude and longitude) for each item, it works for several results but not for all.
data () {
return {
items:[],
show: false,
cities: cities,
newCity:''
}
},
components: {
Item
},
computed: {
sortHumidex() {
return this.items.slice().sort((a,b) => {
return this.getHumidex(b) - this.getHumidex(a) || b.current.temp_c - a.current.temp_c
})
}
},
methods: {
addCity() {
if (this.newCity.trim().length == 0) {
return
}
this.cities.push(this.newCity)
this.newCity = ''
},
getHumidex: (el) => {
const e = 6.112 * Math.pow(10,(7.5*el.current.temp_c/(237.7+el.current.temp_c)))
*(el.current.humidity/100)
return Math.round(el.current.temp_c + 5/9 * (e-10))
},
indexGeo: (e) => {
const lat = Math.round(Math.abs(e.location.lat))
const lon = Math.round(Math.abs(e.location.lon))
return lat.toString() + lon.toString()
},
getApi: function () {
const promises = [];
this.cities.forEach(function(element){
const myUrl = apiUrl+element;
promises.push(axios.get(myUrl))
});
let self = this;
axios
.all(promises)
.then(axios.spread((...responses) => {
responses.forEach(res => self.items.push(res.data))
}))
.catch(error => console.log(error));
}
},
created() {
this.getApi()
this.show = true
}
}
The render when I update API :
By pushing to the existing array of items, you have to deal with the possibility of duplicates. This can be eliminated simply by replacing items every time the API call is made.
Replace:
responses.forEach(res => self.items.push(res.data))
with:
self.items = responses.map(res => res.data)
I am a beginner with Javascript with a bit of knowledge of VueJs. I have an array called tickets. I also have a data api returning two different data objects (tickets and user profiles).
The tickets have user ids and the user profiles has the ids with names.
I needed to create a method that looks at both of that data, loops through it, and assigns the full name of the user to the view.
I was having an issue where my tickets object were not finished loading and it was sometimes causing an error like firstname is undefined. So, i thought I'd try and write an async/await approach to wait until the tickets have fully loaded.
Although my code works, it just doesn't "feel right" and I am not sure how reliable it will be once the application gets larger.
Can I get another set of eyes as to confirmation that my current approach is OK? Thanks!
data() {
return {
isBusy: true,
tickets: [],
userProfiles: [],
}
},
created() {
this.getUserProfiles()
this.getTickets()
},
methods: {
getUserProfiles: function() {
ApiService.getUserProfiles().then(response => {
this.userProfiles = response.data
})
},
getTickets() {
ApiService.getTickets().then(response => {
this.tickets = response.data
this.assignNames(this.tickets)
this.isBusy = false
})
},
// lets wait until the issues are loaded before showing names;
async assignNames() {
let tickets = await this.tickets
var i
for (i = 0; i < this.tickets.length; i++) {
if (tickets[i].assigned_to !== null) {
const result = this.userProfiles.filter(profile => {
return profile.uid == tickets[i].assigned_to
})
tickets[i].assigned_to = result[0].firstname + ' ' + result[0].lastname
}
}
}
}
}
</script>
There are several ways you could do this. Here is the one I prefer without async/await:
created() {
this.load();
},
methods: {
getUserProfiles: function() {
return ApiService.getUserProfiles().then(response => {
this.userProfiles = response.data
})
},
getTickets() {
return ApiService.getTickets().then(response => {
this.tickets = response.data
})
},
load() {
Promise.all([
this.getUserProfiles(),
this.getTickets()
]).then(data => {
this.assignNames();
this.isBusy = false;
});
},
assignNames(){
const tickets = this.tickets;
for (let i = 0; i < this.tickets.length; i++) {
if (tickets[i].assigned_to !== null) {
const result = this.userProfiles.filter(profile => {
return profile.uid == tickets[i].assigned_to
})
tickets[i].assigned_to = result[0].firstname + ' ' + result[0].lastname
}
}
}
}
Forgive me, I'm new to normalizr+redux. I've managed to normalize my data and create a reducer and end up with :
state = {
installations:{
"1":{...},
"2":{...}
}
}
I would then like to filter this data for use in a UI component into two separate categories (in this case where the installation.operator is equal to the current user). I've managed an implementation that works however it seems exhaustive:
const mapStateToProps = (state, ownProps) => {
console.log("mapStateToProps", state.installations);
let assignedInstallations = Object.keys(state.installations)
.filter(i => {
return state.installations[i].operator == state.login;
})
.map(i => {
return state.installations[i];
});
let unassignedInstallations = Object.keys(state.installations)
.filter(i => {
return state.installations[i].operator != state.login;
})
.map(i => {
return state.installations[i];
});
return {
assignedInstallations,
unassignedInstallations,
loginUserId: state.login
};
};
I'm also new to ES6 and am not across all the new syntax shortcuts etc so I suspect there are much better ways to do this.
Is there a more succinct approach with a similar outcome?
you can do this with only one reduce():
const mapStateToProps = (state, ownProps) => {
console.log("mapStateToProps", state.installations);
let {assignedInstallations,
unassignedInstallations } = Object.keys(state.installations)
.reduce(function(acc, cur, i){
if(state.installations[i].operator == state.login){
acc.assignedInstallations.push(state.installations[i]);
}else{
acc.unassignedInstallations .push(state.installations[i]);
}
return acc
}, {assignedInstallations: [], unassignedInstallations: [] })
return {
assignedInstallations,
unassignedInstallations,
loginUserId: state.login
};
};
lodash (An utility library) have a notion of collection (Here is an example https://lodash.com/docs/4.17.4#filter for filter function). It takes as input Object or Array and returns an Array. It seems to fit to your needs. Here is the refactored code:
import {
filter,
} from 'lodash'
const mapStateToProps = (state, ownProps) => {
let assignedInstallations = filter(state.installations, installation => installation.operator == state.login);
let unassignedInstallations = filter(state.installations, installation => installation.operator != state.login);
return {
assignedInstallations,
unassignedInstallations,
loginUserId: state.login
};
};