I have data mapped from an API (see below) which I am reaching fine but I am looking at sorting it numerically (0-9). I'm having a hard time doing this with Vue. If I had my data static in the data(){...}, I can get it done a number of ways. But not from an API because I can't point to the API for some reason whenever I try to point to it from a function in my methods. I have no idea what is going on and I'm hoping you guys have some direction.
The template - Because of the nesting of the API, I am nesting loops as well. (maybe there is a better way to also do this. I'm all ears). allBatches is my Getter. I am serving the API through my State Manager (Vuex)
<div>
<div v-for="batches in allBatches" :key="batches.id">
<div
v-for="dispatchstation in batches.dispatchstation"
:key="dispatchstation.id">
<div v-for="apps in dispatchstation.applications" :key="apps.id">
<div>{{apps}}</div>
</div>
</div>
</div>
</div>
The data structure in the API - I intentionally left unrelated data out. There are other layers in between. But this shows the path I am looping and reaching out to.
"batches": [
{
"dispatchstation": [
{
"applications": [
"384752387450",
"456345634563",
"345634563456",
"567845362334",
"567456745677",
"456734562457",
"789676545365",
"456456445556",
"224563456345",
"456878656467",
"053452345344",
"045440545455",
"045454545204",
"000014546546",
"032116876846",
"546521302151",
"035649874877",
"986765151231",
"653468463854",
"653853121324",
"000145456555"
]
}
]
}
],
I've tried to do this with lodash using _.orderBy and use it a pipe. No luck. And I also tried this:
data() {
return {
sortAsc: true,
sortApps: "" // see explanation
};
},
computed: {
...mapGetters(["allBatches"]),
sortedData() {
let result = this.sortApps;
let ascDesc = this.sortAsc ? 1 : -1;
return result.sort(
(a, b) => ascDesc * a.applications.localeCompare(b.applications)
);
}
},
I used (tried) this method by giving sortApps the loop criteria dispatchstations.applications and loop v-for='apps in sortedData'. I'm sure that is wrong. Vue is not really my forte.
I really don't have any preference on how this should be as long as the data renders sorted out numerically ASC.
Any thoughts?
Thanks
EDIT
Using Chase's answer, I am still getting the data through but it doesn't display. I had to remove the negation (!).
Mutation and getters of State view from the vue chrome tool
EDIT 2 - A simplified version of my store module
import axios from "axios";
const state = {
batches: [],
};
const getters = {
allBatches: state => {
return state.batches;
},
};
const actions = {
async fetchBatches({ commit }) {
const response = await axios.get(`${window.location.protocol}//${window.location.hostname}:4000/batches`);
commit("setBatches", response.data);
},
};
const mutations = {
setBatches: (state, batches) => (state.batches = batches),
};
export default {
state,
getters,
actions,
mutations
};
I hope that I understood your question, so you just need to order the data to render it and you don't need it as ordered in your store?
to display orderd data you can use this computed function, I hope it will help you
computed:{
...mapGetters(["allBatches"]),
orderApplications(){
let copieBatches = JSON.parse(JSON.stringify(this.allBatches));
copieBatches.forEach(batches => {
batches.dispatchstation.forEach(dispatchstation=>{
dispatchstation.applications.sort()
})
});
return copieBatches
}
}
and your HTML will be like
<div>
<div v-for="batches in orderApplications">
<div
v-for="dispatchstation in batches.dispatchstation"
:key="dispatchstation.id">
<div v-for="apps in dispatchstation.applications">
<div>{{apps}}</div>
</div>
</div>
</div>
</div>
Hopefully I'm not misunderstanding your question, but essentially I would recommend loading your data in the same way that you are currently and handling the sort in a computed method.
This is assuming that the length of batches and dispatchstation will always be 1.
new Vue({
el: "#app",
data: {
allBatches: null
},
computed: {
batchesSorted() {
if (!this.allBatches) return {}
const output = this.allBatches.batches[0].dispatchstation[0].applications;
output.sort((a, b) => {
return parseInt(a) - parseInt(b)
})
return output
}
},
async created() {
// Equivelent to ...mapGetters(["allBatches"]) for the example
this.allBatches = {
"batches": [{
"dispatchstation": [{
"applications": [
"384752387450",
"456345634563",
"345634563456",
"567845362334",
"567456745677",
"456734562457",
"789676545365",
"456456445556",
"224563456345",
"456878656467",
"053452345344",
"045440545455",
"045454545204",
"000014546546",
"032116876846",
"546521302151",
"035649874877",
"986765151231",
"653468463854",
"653853121324",
"000145456555"
]
}]
}]
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="(item, key) in batchesSorted" :key="key">
{{ item }}
</div>
</div>
Let me know if I misunderstood anything or if you have any questions.
Related
I have a list objects displayed, each having a checkbox to select it. I also have a checkbox at the top to select the checkbox for every object, kinda like this (assume [ ] is a checkbox):
[ ] Select all
[ ] Object 1
[ ] Object 2
[ ] Object 3
The problem I have is when I have about 100 objects and clicking "Select all", the web page freezes for a good few seconds. There is also a search bar to filter the object, but I tested this by removing it and the performance is just as slow. Each object has a property selected so we know which object is selected. Below are some snippets of my code:
HTML:
<checkbox-input
id="documentSelectAll"
:model-value="operatingRows.length === 0 ? false : allSelected"
#update:modelValue="allSelectPressed" // Calls vuex function below
/>
---
<tr
v-for="(row, index) in operatingRows"
:key="index"
>
<document-table-row
:row-idx="index"
:row-data="row"
:fields="row.fields"
:hidden-column-indices="hiddenColumnIndices"
#toggleSelectedOnRow="toggleSelectedOnRow(row.id)" // Calls vuex to select individual row
/>
</tr>
Computed properties:
operatingRows() {
const showRow = (r) => {
// Some search code, irrelevant here
};
return this.sorted.filter(showRow); // 'sorted' comes from vuex
},
selectedRows() {
return this.operatingRows.filter((r) => r.selected);
},
numSelected() {
return this.selectedRows.reduce((prev, cur) => (cur.selected ? prev + 1 : prev), 0);
},
allSelected() {
return this.numSelected === this.operatingRows.length;
},
Vuex store:
getters: {
...storeStateGetters,
sorted: (state) => state.sorted,
},
---
mutations: {
...storeStateMutations,
SET_ALL_SELECTED_ON_SORTED(state, isSelected) {
state.sorted.map((r) => {
const rn = r;
rn.selected = isSelected;
return rn;
});
},
},
I think it might be to do with the fact that there are too many computed properties? I tried removing them individually (and the associated code) but the performance still seems bad, thus I am not able to pin point the issue to any particular piece of code, rather I think it's to do with the architecture as a whole.
Any help appreciated.
Turns out it was because of the mutation. To fix this, the mutation code was moved to an action which calls a mutation to set the state of the sorted array.
selectOnSorted: ({ commit, rootGetters }, isSelected) => {
const selectedSorted = rootGetters['documents/sorted'].map((doc) => ({
...doc,
selected: isSelected,
}));
commit('SET_SORTED', selectedSorted);
},
I'm new to vue, but I carefully read the docs for vue and vuex as well. But I'm stuck with the following issue. I also do think it's neither that I would not get it done somehow, but I'd like to go for a clean and probaply best practive approach.
The Setup:
- I have some entities
- I have some users
- lets say an entity can belong to multiple users
Now what I want to accomplish is to display a list of all users having those checked which are assigned to the entity.
I have the following vuex store:
export default new Vuex.Store({
state: {
users: [],
entities: []
},
getters: {
USERS: state => state.users,
ENTITIES: state => state.entities,
ENTITY: state => id => {
return state.entities.find(entity => entity.id == id)
}
},
mutations: {
SET_USERS: (state, payload) => {
state.users = payload
},
SET_ENTITIES: (state, payload) => {
state.entities = payload
},
UPDATE_DEVICE: (state, payload) => {
state.entities = state.entities.map(entity => {
if(entity.id === payload.id){
return Object.assign({}, entity, payload.data)
}
return entity
})
}
},
actions: {
GET_USERS: async (context) => {
let { data } = await Axios.get('/api/v1/users')
context.commit('SET_USERS', data)
},
GET_ENTITIES: async (context) => {
let { data } = await Axios.get('/api/v1/entities')
context.commit('SET_ENTITIES', data)
},
UPDATE_ENTITY: ({commit}, payload) => {
Axios.put('/api/v1/entities/' + payload.id + '/', payload.data).then((response) => {
commit('UPDATE_ENTITY', payload)
});
}
}
})
My Entity-Component loads the users from the store within the created hook. The the entity's data is get from the store from the computed property entity(). Also the list of all users is served by a computed property users().
created(){
if(!this.$store.getters.USERS.length) this.$store.dispatch('GET_USERS')
},
computed: {
entity(){
const entityId = this.$route.params.id
return this.$store.getters.ENTITY(entityId)
},
users(){
return this.$store.getters.USERS
}
}
Then within the template I show all the users and a checkbox:
<ul>
<li v-for="(user, i) in users" :key="i">
<input type="checkbox" :value="user" :id="'user_'+i" :name="'user_'+i" v-model="???" />
<label :for="'user_'+i">{{user.name}}</label>
</li>
</ul>
I also have a second list of all users which belong to the entity within the the same component's template like the following which I'd like to keep in sync with the 'selectable list'. So all users with a checked checkbox should be listed in that list:
<ul>
<li v-for="user in entity.users" :key="user.id">
{{user.name}}
</li>
</ul>
And here is where I'm stuck at:
should I use a computed property for the device.users with get() and set() and use this as v-model on the checkboxes? I tried that but it hasn't worked because the user object of the all-users list and the objects of the device.users list were not the same objects even if they represent the same user. And at that point I think I'm doing the whole thing way to complex and I'm simply overlooking the common way practiced vue-users would do it.
So long story short: what is the best way to solve this task?
I think it's a mostly common task.
Thanks for every answer, if more code / details required I of cause will provide them!
How does the structure of entity look? Assuming they have an array 'users', you could calculate the value for the checkbox by providing a basic javascript function that checks if that user's unique ID is in the list for this entity.
In computed make a new property (so you don't recalculate the same array for every element in v-for):
userIdsWithEntity() {
if (!this.entity) return []; // necessary in case entity hasn't been instantiated yet
return this.entity.users.map(x => x.id)
}
Then provide a simple function to the checkbox value that returns true or false: :value="userIdsWithEntity.includes(user.id)"
Instead of v-model (which is :value and #input/#change to update the property provided in :value rolled into one directive, so you might get conflicts with your :value definition), use #change to handle the (un)checking of the checkbox, dispatching an action to vuex to remove/add that user to the entity.
In my Vue application, I have Vuex store modules with large arrays of resource objects in their state. To easily access individual resources in those arrays, I make Vuex getter functions that map resources or lists of resources to various keys (e.g. 'id' or 'tags'). This leads to sluggish performance and a huge memory memory footprint. How do I get the same functionality and reactivity without so much duplicated data?
Store Module Example
export default {
state: () => ({
all: [
{ id: 1, tags: ['tag1', 'tag2'] },
...
],
...
}),
...
getters: {
byId: (state) => {
return state.all.reduce((map, item) => {
map[item.id] = item
return map
}, {})
},
byTag: (state) => {
return state.all.reduce((map, item, index) => {
for (let i = 0; i < item.tags.length; i++) {
map[item.tags[i]] = map[item.tags[i]] || []
map[item.tags[i]].push(item)
}
return map
}, {})
},
}
}
Component Example
export default {
...,
data () {
return {
itemId: 1
}
},
computed: {
item () {
return this.$store.getters['path/to/byId'][this.itemId]
},
relatedItems () {
return this.item && this.item.tags.length
? this.$store.getters['path/to/byTag'][this.item.tags[0]]
: []
}
}
}
To fix this problem, look to an old, standard practice in programming: indexing. Instead of storing a map with the full item values duplicated in the getter, you can store a map to the index of the item in state.all. Then, you can create a new getter that returns a function to access a single item. In my experience, the indexing getter functions always run faster than the old getter functions, and their output takes up a lot less space in memory (on average 80% less in my app).
New Store Module Example
export default {
state: () => ({
all: [
{ id: 1, tags: ['tag1', 'tag2'] },
...
],
...
}),
...
getters: {
indexById: (state) => {
return state.all.reduce((map, item, index) => {
// Store the `index` instead of the `item`
map[item.id] = index
return map
}, {})
},
byId: (state, getters) => (id) => {
return state.all[getters.indexById[id]]
},
indexByTags: (state) => {
return state.all.reduce((map, item, index) => {
for (let i = 0; i < item.tags.length; i++) {
map[item.tags[i]] = map[item.tags[i]] || []
// Again, store the `index` not the `item`
map[item.tags[i]].push(index)
}
return map
}, {})
},
byTag: (state, getters) => (tag) => {
return (getters.indexByTags[tag] || []).map(index => state.all[index])
}
}
}
New Component Example
export default {
...,
data () {
return {
itemId: 1
}
},
computed: {
item () {
return this.$store.getters['path/to/byId'](this.itemId)
},
relatedItems () {
return this.item && this.item.tags.length
? this.$store.getters['path/to/byTag'](this.item.tags[0])
: []
}
}
}
The change seems small, but it makes a huge difference in terms of performance and memory efficiency. It is still fully reactive, just as before, but you're no longer duplicating all of the resource objects in memory. In my implementation, I abstracted out the various indexing methodologies and index expansion methodologies to make the code very maintainable.
You can check out a full proof of concept on github, here: https://github.com/aidangarza/vuex-indexed-getters
While I agree with #aidangarza, I think your biggest issue is the reactivity. Specifically the computed property. This adds a lot of bloated logic and slow code that listens for everything - something you don't need.
Finding the related items will always lead you to looping through the whole list - there's no easy way around it. BUT it will be much faster if you call this by yourself.
What I mean is that computed properties are about something that is going to be computed. You are actually filtering your results. Put a watcher on your variables, and then call the getters by yourself. Something along the lines (semi-code):
watch: {
itemId() {
this.item = this.$store.getters['path/to/byId'][this.itemId]
}
}
You can test with item first and if it works better (which I believe it will) - add watcher for the more complex tags.
Good luck!
While only storing select fields is a good intermediate option (per #aidangarza), it's still not viable when you end up with really huge sets of data. E.g. actively working with 2 million records of "just 2 fields" will still eat your memory and ruin browser performance.
In general, when working with large (or unpredictable) data sets in Vue (using VueX), simply skip the get and commit mechanisms altogether. Keep using VueX to centralize your CRUD operations (via Actions), but do not try to "cache" the results, rather let each component cache what they need for as long as they're using it (e.g. the current working page of the results, some projection thereof, etc.).
In my experience VueX caching is intended for reasonably bounded data, or bounded subsets of data in the current usage context (i.e. for a currently logged in user). When you have data where you have no idea about its scale, then keep its access on an "as needed" basis by your Vue components via Actions only; no getters or mutations for those potentially huge data sets.
I've been stuck on this for a while now and i'm not sure what to Google to find a solution and through the vuejs docs but can't find anything usefull.
Basicly I have a app with a few props. One of them is 'soundpacks'. This prop is setup this way in my app.js:
soundpacks: [
{
title: 'Techno soundpack',
tempo: 130,
genre_id: 1,
teacher_id: 1
},
{
title: 'Deep House soundpack',
tempo: 123,
genre_id: 2,
teacher_id: 2
}
]
As you can see I have declared a genre_id in here. I have another prop declared like this:
genres: [
{
id: 1,
label: 'Techno'
},
{
id: 2,
label: 'Deep House'
}
]
My problem is as follows. On my front-page I want to display all the soundpacks in a div which I do with a v-for. This is working but in this div I want to display the name of the genre that is linked to the soundpack through the genre_id. I have no clue on how to get this label value to display in this v-for and i'm hoping anybody can give me a push in the good direction. I'm new to Vue.Js thats why I'm asking what looks to be a very simple problem.
Any help is appreciated.
Thank you!
I just wrote a computed property that you can add to your component.
Will compute a new list that you can directly use to display in your app.
computed: {
computeListData(){
let newList = []
this.soundpacks.forEach((item) => {
let data = this.genres.filter((genre) => {
return (genre.id === item.genre_id)
})
let listData = item
listData.label = data[0].label
newList.push(listData)
})
return newList
}
}
Here's a gist link that you can refer to:
https://gist.github.com/ayushgupta11/7eeb1a4fdfeadab1a94a1e592ecd53dd
Please tell if this worked for you.
Lots of options here.
Just using a template you can achieve this using nested loops:
<div v-for="pack in soundpacks">
{{ pack.title }} -
<template v-for="genre in genres">
<template v-if="genre.id === pack.genre_id">
{{ genre.label }}
</template>
</template>
</div>
Alternatively, you can use a computed property to add the genre label to your soundpack data:
computed: {
extendedSoundpacks () {
return this.soundpacks.map(pack => {
const genre = this.genres.find(genre => genre.id === pack.genre_id)
return {
// Copy the original object to avoid mutating it.
// Alternatively you could just nest the object by
// removing the three dots, though this would be at
// the expense of a more complicated template.
...pack,
label: genre.label
}
})
}
}
While this is slightly better than the loop in the template it still uses nested looping. Better would be to prepare a map to do a quick lookup of the label from the id:
computed: {
genreMap () {
const genreMap = {}
this.genres.forEach(genre => {
// If genres have more than just a label you could store
// the whole object instead of just the label
genreMap[genre.id] = genre.label
})
return genreMap
}
}
Now we've got genreMap, the find in my earlier extendedSoundpacks example becomes just:
const genre = this.genreMap[pack.genre_id]
Whilst the code isn't much shorter it does avoid the nested looping, which would be much faster if the arrays are large enough.
However, once you have genreMap it becomes pretty trivial to do this in the template without creating a new array of soundpacks:
{{ genreMap[pack.genre_id] }}
In this case that would be sufficient but if the logic were more complicated than a simple id/label lookup you could use a method...
{{ getGenreLabel(pack.genre_id) }}
... or a filter:
{{ pack.genre_id | genreLabel }}
These would need to be defined as functions in the methods or filters sections of your component respectively. If you needed to use this logic across multiple components you might consider registering the filter globally.
<li v-for="soundpack in soundpacks" :key="soundpack.teacher_id">
{{genres[soundpack.genre_id - 1].label}}
</li>
Note that the -1 is because you started your indexing at 1 while the index of an array starts at 0, also note that my code only works if you continue a constant numbering of your genres.
I tried searching a lot of internet but could not find answer to a simple question. I am very new to mithril (do not know why people chose mithril for project :( ). I want to iterate through a list of strings and use its value in drop down with a checkbox.
const DefaultListView = {
view(ctrl, args) {
const parentCtrl = args.parentCtrl;
const attr = args.attr;
const cssClass = args.cssClass;
const filterOptions = ['Pending', 'Paid', 'Rejected'];
// as of now, all are isMultipleSelection filter
const selectedValue =
parentCtrl.filterModel.getSelectedFilterValue(attr);
function isOptionSelected(value) {
return selectedValue.indexOf(value) > -1;
}
return m('.filter-dialog__default-attr-listing', {
class: cssClass
}, [
m('.attributes', {
onscroll: () => {
m.redraw(true);
}
}, [
filterOptions.list.map(filter => [
m('.dropdown-item', {
onclick() {
// Todo: Add click metrics.
// To be done at time of backend integration.
document.body.click();
}
}, [
m('input.form-check-input', {
type: 'checkbox',
checked: isOptionSelected(filter)
}),
m('.dropdown-text', 'Pending')
])
])
])
]);
}
};
Not sure. How to do it. This is what I have tried so far but no luck. Can someone help me this?
At the beginning of the view function you define an array:
const filterOptions = ['Pending', 'Paid', 'Rejected'];
But later on in the view code where you perform the list iteration, filterOptions is expected to be an object with a list property:
filterOptions.list.map(filter =>
That should be filterOptions.map(filter =>.
There may be other issues with your code but it's impossible to tell without seeing the containing component which passes down the args. You might find it more helpful to ask the Mithril chatroom, where myself and plenty of others are available to discuss & assist with tricky situations.