I'm making timeline with users activities in vue template. I would like to list fetched data by grouping it by created_at.
I have fatched data with axios from laravel controller. Resulting array is bellow.
activities:Array[2]
0:Object
causer_id:1
created_at:"2019-09-20 08:55:29"
description:"updated"
id:1
subject_type:"App\User"
1:Object
causer_id:1
created_at:"2019-10-20 08:58:10"
description:"updated"
id:2
subject_type:"App\User"
How to group array by created_at, populate "timeline-day" with value, and then fill "timeline-box" with activities from that day?
<div class="timeline-day">
***created_at***
</div>
<div class="timeline-box" >
<div class="box-content" v-for="(activity, i) in activities" :key="i">
<div class="box-item">{{ activity.subject_type }}</div>
<div class="box-item">{{ activity.description }}</div>
<div class="box-item">{{ activity.created_at }}</div>
</div>
Something like this should do it.
new Vue({
el: '#app',
data () {
return {
activities: [
{
causer_id: 1,
created_at: "2019-09-20 08:55:29",
description: "updated",
id: 1,
subject_type: "App\User"
}, {
causer_id: 1,
created_at: "2019-09-20 09:54:25",
description: "updated",
id: 3,
subject_type: "App\User"
}, {
causer_id: 1,
created_at: "2019-10-20 08:58:10",
description: "updated",
id: 2,
subject_type: "App\User"
}
]
}
},
computed: {
days () {
const map = {}
for (const activity of this.activities) {
const day = activity.created_at.slice(0, 10)
map[day] = map[day] || { created_at: day, activities: [] }
map[day].activities.push(activity)
}
return Object.keys(map).sort().map(day => map[day])
}
}
})
.timeline-day {
border-top: 1px solid #000;
font-weight: bold;
}
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<template v-for="day in days">
<div class="timeline-day">
{{ day.created_at }}
</div>
<div class="timeline-box">
<div class="box-content" v-for="(activity, i) in day.activities" :key="i">
<div class="box-item">{{ activity.subject_type }}</div>
<div class="box-item">{{ activity.description }}</div>
<div class="box-item">{{ activity.created_at }}</div>
</div>
</div>
</template>
</div>
It strips out the date using activity.created_at.slice(0, 10).
For each day it builds up an object containing the date and the array of activities. These are all stored in map. Once it's finished it puts those objects in an array for use by the template.
While I have included a sort() for the dates it doesn't sort the activities, so in practice they would need to already be sorted. You could add in extra sorting if that assumption doesn't hold.
Sorting the activities first lends itself to a slightly different algorithm. The temporary map wouldn't be needed and the objects can just be held in the final array from the get go. As the activities are sorted the only object we need access to is the one at the end of the array. Either the next activity will be added to that last object or a new object needs creating.
Related
I have an array with a list of definitions. Using Vue.js, what is the best way to loop through that array and create a glossary list with letters as the categories?
Desired Output:
A
Aterm: A definition of aterm
B
Bterm: A definition of bterm
C
Cterm: A definition of cterm
Cterm: A definition of cterm
Cterm: A definition of cterm
Y
Yterm: A definition of yterm
Yterm: A definition of yterm
Z
Zterm: A definition of zterm
<div id="app" class="container">
<div v-for="(item, index) in fields" :key="index">
<span>{{ item.Term.charAt(0) }}
<h3>{{ item.Term }}</h3>
<p>{{ item.Definition }}</p>
</div>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
parentMessage: 'Parent',
fields: [
{ Term: 'Aterm', Definition: 'A definition for aterm' },
{ Term: 'Bterm', Definition: 'A definition for bterm' },
{ Term: 'Cterm', Definition: 'A definition for cterm' },
{ Term: 'Cterm', Definition: 'A definition for cterm' },
{ Term: 'Cterm', Definition: 'A definition for cterm' },
{ Term: 'Mterm', Definition: 'A definition for mterm' },
{ Term: 'Yterm', Definition: 'A definition for yterm' },
{ Term: 'Yterm', Definition: 'A definition for yterm' },
{ Term: 'Zterm', Definition: 'A definition for zterm' }
]
},
methods: {
// do something
}
})
</script>
Create a computed property that groups the fields alphabetically by letter (using Array.prototype.reduce() on this.fields[]):
new Vue({
computed: {
fieldsByLetter() {
const groups = this.fields.reduce((p, field) => {
const letter = field.Term.at(0).toUpperCase() // use uppercase first letter
p[letter] ??= [] // create array for this letter if needed
p[letter].push(field) // add field to array
return p // return updated object
}, {})
// alphabetize fields by Term
Object.values(groups).forEach(fields => fields.sort((a, b) => a.Term.localeCompare(b.Term)))
return groups
}
},
})
Then iterate the computed object with v-for, rendering the object's fields[] with its own v-for, and each item in a <li> to get the desired output:
<div id="app" class="container">
<div v-for="(fields, letter) in fieldsByLetter" :key="letter">
<h3>{{ letter }}</h3>
<ul>
<li v-for="item in fields" :key="item.Term">
<span>{{ item.Term }}:</span>
<span>{{ item.Definition }}</span>
</li>
</ul>
</div>
</div>
demo
This is the simplest way I can offer. I'll benefit lodash to solve the problem.
First, you need to import lodash on the top of the script code. It is a powerful tool for manipulating data structure based on my experience.
import * as _ from "https://cdn.skypack.dev/lodash#4.17.21";
You need to add this code:
methods: {
sorting(item) {
return _.mapValues(
_.groupBy(item, function (e) {
return e.Term;
})
);
},
},
computed: {
terms() {
return this.sorting(this.fields);
},
}
Here, I made a computed variable to manipulate the variable called fields by calling the sorting function which implements lodash. I mapped the values and grouped them based on the Term field in the array.
Then, you need to restructure the html code:
<div id="app" class="container">
<div v-for="(term, key, index) in terms" :key="index"> <!-- Replace fields with the computed variable terms, and access the keys -->
<h4>{{ key.charAt(0) }}</h4> <!-- Call the key with the first Index -->
<ul> <!-- Add this -->
<li v-for="item in fields" :key="item.Term">
<span>{{ item.Term }}:</span>
<span>{{ item.Definition }}</span>
</li>
</ul>
</div>
</div>
Codepen: https://codepen.io/auliaamir/pen/GROVJvr
i have a number field, that i want to bind value from an array like this :
<div v-for="p in products">
<input type="number" :value="carModel[p.name]" />
</div>
where p.name is a string (product name), car_a :
const app = Vue.createApp({
data() {
return {
products: {...},
carModel:[{car_a:4}, {car_b:2} ]
}
}
...
But this does not works, the input remains empty, while following runs without issue :
<div v-for="p in products">
<input type="number" :value="carModel" />
</div>
const app = Vue.createApp({
data() {
return {
products: {...},
carModel:4
}
}
...
So, my question is, how to bind the value properly from array if i have the key ?
thank you
First off, I wonder if your carModel array item object structure could be improved; perhaps better would be something like:
products: ["car_a", "car_b"],
carModel: [
{
name: "car_a",
value: 4,
},
{
name: "car_b",
value: 2,
},
{
name: "car_c",
value: 3,
},
]
I assume that the Strings held in the products array are a sub-set of the Strings in the carModel array of objects, and since you want an input that is reactive with the data, you will want the input's model to be the values held by each carModel object. So rather than v-for loop over the products array, v-for loop over the carModel but filter out the elements whose Strings are not held by the products array. Since we should not combine v-for with v-if, this filtering should be done in a computed property:
computed: {
filteredCarModel() {
return this.carModel.filter(cm => {
return this.products.includes(cm.name);
});
}
},
so then in the HTML template you can loop over the computed property:
<div v-for="cm in filteredCarModel" :key="cm.name">
<label :for="cm.name">{{ cm.name }} </label>
<input type="number" :id="cm.name" v-model="cm.value" />
</div>
This is key:
This will display proper values in the input elements, and these elements will remain reactive to the model such that changes to the inputs will cause changes to the data model, and visa-versa. Thus the display will be truly bound to the data model, which is what you're asking and what Vue.js is all about.
Here is a sample HTML showing that the data is in fact reactive:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<html>
<head>
<meta charset=utf-8" />
<title>Vue Example</title>
<script src="https://unpkg.com/vue#next"></script>
</head>
<body>
<h1>Vue Example</h1>
<div id="app">
<h2>Show Data</h2>
<div v-for="cm in carModel" :key="cm.name">
{{ cm.name }} : {{ cm.value }}
</div>
<h2>Change Data</h2>
<div v-for="cm in filteredCarModel" :key="cm.name">
<label :for="cm.name">{{ cm.name }} </label>
<input type="number" :id="cm.name" v-model="cm.value" />
</div>
</div>
<script>
Vue.createApp({
data() {
return {
products: ["car_a", "car_b"],
carModel: [
{
name: "car_a",
value: 4,
},
{
name: "car_b",
value: 2,
},
{
name: "car_c",
value: 3,
},
],
};
},
computed: {
filteredCarModel() {
return this.carModel.filter((cm) => {
return this.products.includes(cm.name);
});
},
},
}).mount("#app");
</script>
</body>
</html>
</html>
Note that if the products array is not a subset of the carModel array, if all the Strings present in products are also present in carModel name fields, then there will be no need to have a filteredCarModel() computed property.
change your carModel to hash
carModel: { 'car_a': 4, 'car_b': 2 }
because what you are trying to do is access array by string(which it should be number as index of the array)
Observations :
products should be an array to iterate via v-for.
As carModel is an array. You can not access object properties directly. Access it via index.
Working Demo :
new Vue({
el: "#app",
data: {
products: [{name: 'car_a'}, {name: 'car_b'}],
carModel:[{car_a: 4}, {car_b: 2}]
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="(product, index) in products" :key="index">
<input type="number" :value="carModel[index][product.name]"/>
</div>
</div>
In my Vue application, I have a very small line of code based on a v-for loop and it works until I throw a V-IF into it.
THe following works:
<div v-for="date in dates" :key="date">
<th v-for="store in stores">#{{store.stock}}</th>
</div>
However, when I try to get it to only show that value if the date in the object matches the dates object, I get that store is undefined
<div v-for="date in dates" :key="date">
<div v-for="store in stores">
<th v-if="store.date === date">#{{store.stock}}</th>
</div>
</div>
Here are my objects:
stores: [
{
store: "123",
date: "2021-09-01",
stock: "145"
}
]
dates: [
{
date: "2021-09-01"
}
]
Why am I having such an issue when trying to match the dates in the v-if?
instead of use v-if with v-for you can use computed to get store you need display and then use just one v-for
<template>
<div>
<div
v-for="store in activeStore"
:key="store.stock"
>
{{ store.date }}
</div>
</div>
</template>
<script>
export default ({
data() {
return {
count: 1110,
stores: [{
store: 'test',
date: '2021-09-01',
stock: '145'
}, {
store: 'test',
date: '2021-09-02',
stock: '146'
}, {
store: 'test',
date: '2021-09-03',
stock: '147'
}, {
store: 'test',
date: '2021-09-04',
stock: '148'
}],
dates: ['2021-09-01', '2021-09-02', '2021-09-03', '2021-09-00']
}
},
computed: {
activeStore: function() {
return this.stores.filter((store) => {
if (this.dates.includes(store.date)) {
return store
}
})
}
}
})
</script>
You made a little mistake. date should be date.date!
<div v-for="date in dates" :key="date">
<div v-for="store in stores">
<th v-if="store.date === date.date">#{{store.stock}}</th>
</div>
</div>
Here is a simple solution using computed property
<div v-for="date in dates" :key="date">
<div v-for="(store, indexer) in getFilteredStores(date)" :key="indexer">
<th>#{{store.stock}}</th>
</div>
</div>
and in your computed, define the below handler to get the store for particular dates
computed: {
getFilteredStores() {
return targetDate => { // targetDate is the argument passed to the handler
return this.stores.filter(store => store.date === targetDate);
}
}
}
vue discourages the use of v-if inside a v-for
it is much better to use a computed property that filters the list and in the
v-for you pass the already filtered list
So now i'm doing this to organise my results by category but if feel like this could be better:
<div><h2>Gloves</h2></div>
<div v-for="stash in stashes" :key="stash.id">
<div v-for="item in stash.items" :key="item.id">
<div v-if="item.extended.subcategories[0] === 'gloves'">
{{ item.extended.baseType }}
</div>
</div>
</div>
<div><h2>Boots</h2></div>
<div v-for="stash in stashes" :key="stash.id2">
<div v-for="item in stash.items" :key="item.id2">
<div v-if="item.extended.subcategories[0] === 'belt'">
{{ item.extended.baseType }}
</div>
</div>
</div>
<div><h2>Helmets</h2></div>
..
<div><h2>Weapons</h2></div>
..
If found this article doing this with a computed property and i feel like this should be the way but can't get it to work (also because i need a argument for it to work this way i think?):
computed: {
filter(category) {
return this.stashes.items.filter(a => a.extended.subcategories[0] === category);
}
}
and then something like this:
<div v-for="item in filter('gloves')" :key="item.id">
..
</div>
But yeah, it says i can't pass a argument in that for loop like this so that is where i ended for now.
Anyone got an idea how to do this?
Stashes looks like this:
stashes: [
{
id: 1
items: [{
name: 'lorem',
extended: {
subcategories: ["gloves"]
}
}]
},
{
id: 2
items: [{
name: 'ipsum',
extended: {
subcategories: ["boots"]
}
}]
},
]
While using a method in the template might solve this, it's not a good pattern because it causes the method to run every time the template is rerendered for any reason. Add another level of v-for:
<div v-for="category in categories" :key="category">
<div><h2>{{ category }}</h2></div>
<div v-for="stash in stashes" :key="stash.id">
<div v-for="item in stash.items" :key="item.id">
<div v-if="item.extended.subcategories[0] === category">
{{ item.extended.baseType }}
</div>
</div>
</div>
</div>
And create an array of categories like:
data() {
return {
categories: ['gloves','belt']
}
}
You can achieve this by returning a method from your computed but I do not recommend this solution. Instead of computed I recommend you to use a method.
[RECOMENDED]
method: {
filter(category) {
return this.stashes.items.filter(a => a.extended.subcategories[0] === category);
}
}
[USING COMPUTED]
computed: {
filter() {
return category => this.stashes.items.filter(a => a.extended.subcategories[0] === category);
}
}
Here you can read a little more about this: Why I can not pass parameter to the computed
In vue.js, in my data object I have an object of items that are sorted in one order. I want to make it in decending order.
My vue template looks like below snippet -
<div class="form-element form-element-checkbox" :key="key" v-for="(value, key) in items">
<input
:id="getID(value)"
type="checkbox"
:value="value"
#change="updateFilter"
v-model="selections"
:checked="isSelected(value)">
<label class="chk-small" :for="getID(value)">
<span :aria-label="`Rated ${key}.0 out of 5`" class="star-rating noBlank" :style="{ width: `${key * 20}px` }"></span>
</label>
</div>
And items object looks like
data() {
return {
items: {
5: 'item_5',
4: 'item_4',
3: 'item_3'
},
selections: []
};
},
Storing data in that order does't work.Tried to use items.slice().reverse() also. What might be wrong here ? I'm not able to get.
If you cannot / do not want to change your items dictionary into an array, you can simply use Object.keys() to extract its keys and work on them.
new Vue({
el: '#app',
data() {
return {
items: {
// Note: even though you write them in descending order,
// the JS engine will list them in ascending order.
5: 'item_5',
4: 'item_4',
3: 'item_3',
},
};
},
});
<script src="https://unpkg.com/vue#2"></script>
<div id="app">
<p>itemsKeys: {{Object.keys(items)}}</p>
<ol>
<li v-for="(key, keyIndex) of Object.keys(items).reverse()">
keyIndex: {{keyIndex}} / item key: {{key}} / item value: {{items[key]}}
</li>
</ol>
</div>