Maintain associated objects in Vue - javascript

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.

Related

Redux toolkit filter() not working when wanting to display new array at the same time

I am currently building a clothing shop in which you can add products to the cart, and delete each one of them as you like and it makes the cart re-render and display a new cart without that product within it.
so I've made a Slice for cart in redux. the 'addProduct' part works fine, but the 'deleteProduct' reducer which uses filter() doesn't work. (when i click my delete button nothing happens, and no changes in difference in Redux Devtools)
my slice:
const selectedProductsSlice = createSlice({
name:'selectedProducts',
initialState:{
productList:[],
checkoutPrice:0,
},
reducers: {
addProduct:(state,{payload}) => {
state.productList.push(payload)
state.checkoutPrice += payload.price
},
deleteProduct:(state,{payload}) => {
state.productList.filter((foundProduct) => foundProduct.id !== payload.id)
}
}});
my button and handleDelete:
<button className="btn-delete" onClick={handleDelete(product)}>Delete</button>
function handleDelete(p) {
console.log(p)
dispatch(deleteProduct(p.product))
}
edit:
filter didnt work in every possible way i tried. so i changed my way and did this instead to work. but still i wonder why didnt filter() method work properly.
deleteProduct: (state, { payload }) => {
const foundIndex = state.productList.findIndex((p) => {
return p.product.id === payload.id
})
state.productList.splice(foundIndex,1)
}
The filter operation creates a new array without changing the existing instance. Therefore you need to assign it to state.
deleteProduct: (state, { payload }) => {
return {
...state,
productList: state.productList.filter(
foundProduct => foundProduct.id !== payload.id
)
};
};
You also need to change the way you call the handleDelete in the onClick handler.
onClick={() => handleDelete(product)}
Filter methods returns new array so you have to update your existing array tool. Please update your slice:
const selectedProductsSlice = createSlice({
name:'selectedProducts',
initialState:{
productList:[],
checkoutPrice:0,
},
reducers: {
addProduct:(state,{payload}) => {
state.productList.push(payload)
state.checkoutPrice += payload.price
},
deleteProduct:(state,{payload}) => {
state.productList=state.productList.filter((foundProduct) => foundProduct.id !== payload.id)
}
}});
I went through exactly same problem as you.
I solved this problem just to move my reducer to another existing reducer
Hope you to get through it. I couldn't pass it by

Vuejs return data from component to Laravel

I am trying to make a search bar to collect a list of products which the user will then be able to select an array of products and add it to an order.
So far the products can be searched, added to a "products" array via an "add" button and they are able to see the data within products. They are also able to remove products that they do not want.
My issue is that I am trying to send the data from the "products" array with a parent form. The search component is located within the form as well as the list. But I do not know how I would send the array with the form.
I do not need to send the entire product object, but just the ID which will be used to link up the products with the order.
Here is where the component is:
<div class="uk-margin" id="search">
<label for="Search" class="uk-form-label">Search products to add</label>
<search input="ess"></search>
</div>
Here is the vuejs component data:
export default {
data() {
return {
keywords: null,
results: [],
products: []
};
},
watch: {
keywords (after, before) {
this.fetch();
}
},
methods: {
fetch() {
axios.get('/search', { params: { keywords: this.keywords } })
.then(response => this.results = response.data)
.catch(error => {});
},
addProduct (product) {
let duplicate = false;
this.products.forEach((item, index) => {
if (item.ID === product.ID) {
duplicate = true;
}
});
if (!duplicate) {
this.products.push(product);
}
},
removeProduct (product) {
Vue.delete(this.products, product);
}
}
}
Everything works fine, but my question is.. How am I able to pass the data back to the html / laravel to use it while sending data to the controller. I only need to send the "products" array, I have tried using input with the data but it isn't working. Does anyone know how I could do it, or is the best way to do so, using JavaScript and add them to an array by finding all the elements which are being displayed?
Many thanks.

How to list multiple objects from laravel to vue using javascript filter function?

I am passing multiple objects from laravel to vue and want to populate my vue objects with values given from the database.
The data from laravel:
Vue objects:
storeName: {},
storeUrl: {},
appName: {},
price: {},
share: {},
Where the data comes:
mounted() {
axios.get('/dashboard/affiliates')
.then(res => {
let data = res.data;
this.storeName = data.affiliates.shop_name;
console.log(data.affiliates);
})
As I understand, one of the bests ways would be to populate my vue objects it would be with filter function of javascript, but quite don't get it fully and don't know how to do it.
How could I populate vue objects with data that later on I could list them to the view in a v-for?
First use a compute function to link view and when you change storeName the app detect changes and paint again
Second, to make a array of shop_name use .filter to filter and them a .map to adapt your data
My advice is you store all data on vue data side, and on your computed method filter data and optional parse data, Also you can access to data with template, so you can parse data on template.
Something like this
export default {
name: 'loquesea',
data: {
affiliates: []
},
mounted() {
axios.get('/dashboard/affiliates')
.then(res => {
let data = res.data;
this.affiliates = data.affiliates;
})
},
computed: {
getStoresName() {
return this.affiliates.filter(() => {
// what you want
return true;
}).map((affiliate) => {
return affiliate.shop_name;
})
}
}
}
on your template.html
<ul id="example-1">
<li v-for="storeName in getStoresName">
{{ storeName }}
</li>
</ul>

Is there a standard way to add presentation information to Vue?

Let's say I have a Vue component that has the following data, which has been retrieved from an API:
data: () => ({
books: [
{name: 'The Voyage of the Beagle', author: 'Charles Darwin'},
{name: 'Metamorphoses', author: 'Ovid'},
{name: 'The Interpretation of Dreams', author: 'Sigmund Freud'},
],
}),
I would like to store presentation variables for each of these books, e.g. an open boolean to determine whether the book is open or not. I don't want the API to return these variables though, as I don't want the API to be cluttered with presentation data.
Is there a standard way of doing this in Vue?
you can add the presentation data after receive the information from the API:
...
data: () => ({ books: [] });
...
methods: {
// API call to get the books
async requestBooks() {
// TODO: add try catch block
const books = await getBooks(); // Your API call
this.books = addPresentationInformation(books);
},
addPresentationInformation(books) {
return books.map(book => {
return {
...book, // default format from API (name, author)
open: false, // add the open variable to the object
reading: false,
currentPage: 0
}
});
}
},
created() {
this.requestBooks(); // Call the api on created hook to initialize the books data prop
}
You can add many presentation variables as you want, I recommend use vuex to store the books and their presentation variables, that way you can save information in the local storage for each book, so after restart the app, you can know if some book is currently being reading or is open.
I would personally maintain another array that contains some state relational to each book rather than trying to mutate the API response data. That's just me though.
Probably another way is to copy object and modify it and keep original response data
data(){
let data = Object.assign({}, this);
// add necessary presentation data
return data;
}
I now use normalizr to process and flatten responses from the backend API, and this library provides a means to add extra data. For example, the following schema adds the hidden data attribute.
const taskSchema = new schema.Entity(
'tasks',
{},
{
// add presentation data
processStrategy: (value) => ({
...value,
hidden: false
}),
}
);

Efficiently working with large data sets in Vue applications with Vuex

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.

Categories

Resources