I am looking for a way to group objects in different DIV's with a header text.
The following allows me to group 'category 'bla1' but I'd like to have categories 'bla2' and 'bla3' to be grouped as well. The number of categories isn't fixed and can be less or more.
This is what I have so far: JSFIddle
<div id="app">
<h2>Todos:</h2>
<ol>
<div v-for="todo in todos">
<p v-if="todo.category == 'bla1'">
{{ todo.text }}
</p>
</div>
</ol>
</div>
new Vue({
el: "#app",
data: {
todos: [
{ text: "Learn JavaScript", category: "bla1" },
{ text: "Learn Vue", category: "bla2" },
{ text: "Play around in JSFiddle", category: "bla1" },
{ text: "Build something awesome", category: "bla3" }
]
}
})
resulting is:
Todos:
Learn JavaScript
Play around in JSFiddle
But the desired result is:
Todos:
bla1
Learn JavaScript
Play around in JSFiddle
bla2
Learn Vue
bla3
Build something awesome
Create a computed property to group you list by category, and then do a nested loop in your template:
computed: {
groupedToDos() {
let groupedToDos = {};
this.todos.forEach(todo => {
if (todo.category in groupedToDos) {
groupedToDos[todo.category].push(todo)
}
else {
groupedToDos[todo.category] = [todo]
}
})
return groupedToDos
}
}
And then in your template:
<div id="app">
<h2>Todos:</h2>
<ol>
<div v-for="(todos, key) in groupedToDos">
<h3>{{ key }}</h3>
<p v-for="todo in todos">
{{ todo.text }}
</p>
<br />
</div>
</ol>
</div>
jsfiddle
The computed property approach is the best IMHO.
computed:{
sampleComputed(){
if (this.sampleData.value === "Sample 1") return "Sample 1 Value"
if (this.sampleData.value === "Sample 2") return "Sample 2 Value"
return "Sample 0 Value"
}
}
Above sample code can be used with Switch case also if you have more than 5 conditions, or even using object literals to fetch returning data to improve time complexity.
However, if you want you can add more v-if inside the template itself, but it is not advisable to do so since you should focus on separating complex logic from template as a best practice.
References: Proper way to express switch statement with Vue data bindings
Related
I have a component that accepts props
<BarChart
header="header name"
dataPoint="data_to_look_at"
size=35
/>
With the dataPoint prop I want to use this in my component so that I can use it (I think, idk if this is the right solution) in an interpolated string like this to access items in an object
// inside of a v-for loop that iterates over an object etc
{{ data[index].attributes.${dataPoint} }}
I'm not sure how to do this and of course the above doesn't work
string interpolation Vue js
Not relevant to my question
How can I solve "Interpolation inside attributes has been removed. Use v-bind or the colon shorthand"? Vue.js 2
Not quite it either
How do interpolate a prop in a interpolation?
Observation : As you are iterating your item list by using v-for loop, No need to access the item by index. You can simply do like this :
<p v-for="data in items" :key="data.id">
{{ data.attributes[datapoint] }}
</p>
Live Demo :
Vue.component('child', {
data: function() {
return {
items: [{
id: 1,
attributes: {
name: 'Alpha'
}
}, {
id: 2,
attributes: {
name: 'Beta'
}
}, {
id: 3,
attributes: {
name: 'Gamma'
}
}]
}
},
props: ['header', 'datapoint'],
template: `<div>
<p v-for="data in items" :key="data.id">{{ data.attributes[datapoint] }}</p>
</div>`
});
var app = new Vue({
el: '#app'
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<child header="Header Name" dataPoint="name"></child>
</div>
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
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
I have a v-for like so:
<p> User Responses(s):</p>
<template v-for="item in UserResponses">
<ol v-if="item.some_condition==item._is_true">
<li :name="item.id + '__UR'"> [[ item.some_variable_to_render ] ]] </li>
</ol>
</template>
Works great. However, what I would like to do is say No user responses when the some_condition_is_true fails, but if I add an v-else the message (No user responses) will be printed in every loop, which is not desired ofcourse. How does one solve this problem?
To this end, I wondered if I could test if the element item.id + '__UR'" is present and if not add an element with text sayingNo user responses`
I am not sure however that this is the correct way to go about this.
EDIT
Just to reiterate: using v-if before v-for is not an option since the object being iterated on is a nesed JSON which then is filtered through some vuejs filters, so yea, this is not an option.
note that boolVar corresponds to your actual object key that contains the bools
let model.flag = true
function foo(val) => {
model.flag = val
return val
}
<p> User Responses(s):</p>
<template>
<div v-if="model.flag">
<div v-for="item in UserResponses" >
<ol v-if="foo(item.Boolvar)" >
<li :name="item.id + '__UR'"> [[ item.some_variable_to_render ] ]] </li>
</ol>
</div>
</div>
<h1 v-else>No user responses 😢</h1>
</template>
It is possible you may be able to implement this using CSS:
ol + .no-responses {
display: none;
}
There's a full example below but the idea is simply to hide the message if it comes after an <ol>. Tweak the selector to reflect your real use case.
I have tried to keep the template in my example exactly the same as the original code, with the addition of the relevant message and a couple of buttons for demo purposes.
new Vue({
el: '#app',
delimiters: ['[[', ']]'],
data () {
return {
UserResponses: []
}
},
methods: {
add () {
this.UserResponses.push(
{ some_condition: 3, _is_true: 3, id: 3, some_variable_to_render: 'A' },
{ some_condition: 3, _is_true: 4, id: 4, some_variable_to_render: 'B' },
{ some_condition: 4, _is_true: 4, id: 5, some_variable_to_render: 'C' },
{ some_condition: 5, _is_true: 5, id: 6, some_variable_to_render: 'D' }
)
},
clear () {
this.UserResponses.splice(0)
}
}
})
ol + .no-responses {
display: none;
}
<script src="https://unpkg.com/vue#2.6.11/dist/vue.js"></script>
<div id="app">
<p>User Responses(s):</p>
<template v-for="item in UserResponses">
<ol v-if="item.some_condition==item._is_true">
<li :name="item.id + '__UR'"> [[ item.some_variable_to_render ]] </li>
</ol>
</template>
<p class="no-responses">No user responses</p>
<button #click="add">Add values</button>
<button #click="clear">Clear values</button>
</div>
Well I hope to explain, I'm generating this data from a component, when I click the checkbox changes in the generated data are not reflected, but when clicking the button with a data already in the instance changes are made, I appreciate if you explain Why or do they have a solution?
this my code
js
Vue.component('fighters', {
template: '#fighters-template',
data() {
return {
items: [
{ name: 'Ryu' },
{ name: 'Ken' },
{ name: 'Akuma' }
],
msg: 'hi'
}
},
methods: {
newData() {
this.items.forEach(item => {
item.id = Math.random().toString(36).substr(2, 9);
item.game = 'Street Figther';
item.show = false;
});
this.items.push()
},
greeting() {
this.msg = 'hola'
}
}
});
new Vue({
el: '#app'
})
html
<main id="app">
<fighters></fighters>
</main>
<template id="fighters-template">
<div class="o-container--sm u-my1">
<ul>
<li v-for="item in items">
<input type="checkbox" v-model="item.show">
{{ item.name }}</li>
</ul>
<button class="c-button c-button--primary" #click="newData()">New Data</button>
<h2>{{ msg }}</h2>
<button class="c-button c-button--primary" #click="greeting()">Greeting</button>
<hr>
<pre>{{ items }}</pre>
</div>
</template>
this live code
https://jsfiddle.net/cqx12a00/1/
Thanks for you help
You don't declare the show variables that your checkboxes are bound to, so they are not reactive – Vue is not aware when one is updated.
It should be initialized like so:
items: [
{ name: 'Ryu', show: false },
{ name: 'Ken', show: false },
{ name: 'Akuma', show: false }
]
You might think that newData would fix it, since it assigns a show member to each item, but Vue cannot detect added properties so they're still not reactive. If you initialized the data as I show above, then the assignment would be reactive.
If you want to add a new reactive property to an object, you should use Vue.set.