Vue.js - Nested v-for Loops - javascript

I have a Vue 3.0 app. In this app, I have the following:
export default {
data() {
return {
selected: null,
departments: [
{
name: 'Sports',
items: [
{ id:'1', label: 'Football', value:'football' },
{ id:'2', label: 'Basketball', value:'basketball' },
]
},
{
name: 'Automotive',
versions: [
{ id:'3', label: 'Oil', value:'oil' },
{ id:'4', label: 'Mud Flaps', value:'mud-flaps' },
]
}
]
};
}
};
I need to render these items in a dropdown list. Notably, the items need to be rendered by sections. In an attempt to do this, I'm using Bootstrap's dropdown headers. My challenge is, the HTML needs to be rendered in a way that I can't see how to do with Vue. I need to render my HTML like this:
<ul class="dropdown-menu" aria-labelledby="dropdownMenuLink">
<li><h6 class="dropdown-header">Sports</h6></li>
<li><a class="dropdown-item" href="#">Football</a></li>
<li><a class="dropdown-item" href="#">Basketball</a></li>
<li><h6 class="dropdown-header">Automotive</h6></li>
<li><a class="dropdown-item" href="#">Oil</a></li>
<li><a class="dropdown-item" href="#">Mud Flaps</a></li>
</ul>
Based on this need, I would have to have a nested for loop. In psuedocode, it would be like this:
foreach department in departments
<li><h6 class="dropdown-header">{{ department.name }}</h6></li>
foreach item in department.items
<li><a class="dropdown-item" href="#">{{ item.label }} </a></li>
end foreach
end foreach
I know I could do this with something like Razer. However, I don't see anyway to do this in Vue. Yet, I have to believe I'm overlooking something as rendering a hierarchy is a common need. How do you render a hierachy in Vue without changing the data structure?
Thank you.

just use nested v-for like this:
<template v-for="(department , i) in departments">
<li><h6 class="dropdown-header" :key="`head-${i}`">{{ department.name }}</h6></li>
<template v-for="(item , j) in department.items">
<li><a class="dropdown-item" href="#" :key="`sub-${i}${j}`">{{ item.label }} </a></li>
</template>
</template>

you can have nested v-for with the help of template and also make sure that the keys binding to the elements are unique (this has to be unique so you don't get weird behavior from vue).
run the code below and check the result (click on full screen to see the full list rendered):
// you can ignore this line
Vue.config.productionTip = false;
new Vue({
el: '#app',
data: {
departments: [{
name: 'Sports',
items: [{
id: '1',
label: 'Football',
value: 'football'
},
{
id: '2',
label: 'Basketball',
value: 'basketball'
},
]
},
{
name: 'Automotive',
items: [{
id: '3',
label: 'Oil',
value: 'oil'
},
{
id: '4',
label: 'Mud Flaps',
value: 'mud-flaps'
},
]
}
],
},
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<ul class="dropdown-menu" aria-labelledby="dropdownMenuLink">
<template v-for="({name, items}, i) in departments">
<li :key="`header-${i}`">
<h6 class="dropdown-header">{{ name }}</h6>
</li>
<li v-for="{label, id} in items" :key="`link-${id}`">
<a class="dropdown-item" href="#">{{ label }}</a>
</li>
</template>
</ul>
</div>

One approach to this is to use a computed method. I've created a jsfiddle here.
computed: {
departmentItems() {
const items = [];
this.departments.forEach(department => {
items.push({
label: department.name,
class: 'dropdown-header',
});
department.items.forEach(item => {
items.push({
label: item.label,
class: 'dropdown-item'
})
})
})
return items;
}
}
The important part here is, that we have a common model for both dropdown-header and dropdown-item.

You structure your data in a computed property, and then use v-for to iterate over the list. Here is an example:
new Vue({
el: "#app",
data() {
return {
selected: null,
departments: [
{
name: 'Sports',
items: [
{ id:'1', label: 'Football', value:'football' },
{ id:'2', label: 'Basketball', value:'basketball' },
]
},
{
name: 'Automotive',
items: [
{ id:'3', label: 'Oil', value:'oil' },
{ id:'4', label: 'Mud Flaps', value:'mud-flaps' },
]
}
]
}
},
computed: {
departmentsItem: function() {
return this.departments.reduce((acc,department) =>
[
...acc,
{ type: "department", name: department.name },
...department.items.map(item => ({ type: "item", name: item.label }))
]
, []);
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<ul class="dropdown-menu" aria-labelledby="dropdownMenuLink">
<li v-for="(departmentItem,i) in departmentsItem" :key="i">
<h6
v-if="departmentItem.type==='department'" class="dropdown-header"
>{{departmentItem.name}}</h6>
<a
v-else-if="departmentItem.type==='item'" class="dropdown-item" href="#"
>{{departmentItem.name}}</a>
</li>
</ul>
</div>

Related

How to make nested infinite v-for in vue?

this is what i have tried
Html
<ul class="tree">
<li>
<span>First Node</span>
<ul>
<li v-for="(tree, i) in trees" :key="i">
<span v-text="tree.name"></span>
<ul v-if="tree.childrens">
<li v-for="(child, j) in tree.childrens" :key="j">
<span v-text="child.name"></span>
</li>
</ul>
</li>
</ul>
</li>
</ul>
Javascript
I wanna loop every children without adding a new HTML element, but I have no idea how to do that.
export default {
data() {
return {
trees: [
{ name: 'Node A' },
{ name: 'Node B', childrens: [
{ name: 'Node B1' },
{ name: 'Node B2', childrens: [
{ name: 'Node B2.1' },
{ name: 'Node B2.2' },
] },
] },
],
};
},
};
Display
Anyone, thanks for your help
You can achieve this requirement by using Circular References Between Components concept of Vue. Where instead of creating a separate element for each children you can recursively call the child component.
Live Demo :
Vue.component('child', {
props: ['children'],
template: `<ul>
<li v-for="(child, index) in children" :key="index">
<parent v-if="child.childrens" :trees="[child]"></parent>
<span v-else>{{ child.name }}</span>
</li>
</ul>`
});
Vue.component('parent', {
props: ['trees'],
template: `<ul>
<li v-for="(tree, i) in trees" :key="i">
<span v-text="tree.name"></span>
<child v-if="tree.childrens" :children="tree.childrens"></child>
</li>
</ul>`
});
var app = new Vue({
el: '#app',
data: {
trees: [
{ name: 'Node A' },
{ name: 'Node B', childrens: [
{ name: 'Node B1' },
{ name: 'Node B2', childrens: [
{ name: 'Node B2.1' },
{ name: 'Node B2.2' },
] },
] },
]
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<parent :trees="trees"></parent>
</div>

Loop throuth a filtered list in vue.js with parameter

How to loop in vue.js through a filtered list with an input parameter given?
Pls. take into consideration that filtering like <li v-for="x in todos_filtered" v-if="x.person == 'Mike'"> should be avoided with many records.
Here the code which returns an empty list instead of 2 elements.
<html>
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.14/dist/vue.js"></script>
<body>
<div id="app">
<ul>
<li v-for="x in todos_filtered('Mike')">
{{ x.text }}
</li>
</ul>
</div>
<script>
myObject = new Vue({
el: '#app',
data: {
todos: [
{ person: 'Mike', text: 'Learn JavaScript' },
{ person: 'Luke', text: 'Learn Vue.js' },
{ person: 'Mike', text: 'Build Something Awesome' }
]
},
computed: {
todos_filtered(person) {
return this.todos.filter((x) => x.person === person);
}
},
})
</script>
</body>
</html>
The computed property should return a function that take the person as parameter:
computed: {
todos_filtered() {
return (person)=> this.todos.filter((x) => x.person === person);
}
},

setting single element to hidden in vue js when mapped

kinda new to vue, I have mapped out some data from my initial data object in vue.js I am trying to hide and show only the items within that iteration of the mapping when the user selects the heading. I am using the isHidden prop in vue to hide and show my list items but when selecting any heading it shows all the tags instead of those associated with that specific header.
anyone know the proper way to do this? should I use some index or id from e.target? or should I give each list item a 'hidden' property and change it that way somehow?
here's my list that I mapped out
<div v-for="item in list">
<h4 v-on:click="viewItemsinCat()">{{item.category}}</h4>
<ul>
<li v-if="!isHidden" v-for="item in item.food">
{{item}}
</li>
</ul>
</div>
then I have my data like so:
data: {
list: [{
category: 'Baked goods',
food: ['bread', 'cookie', 'butter', 'powder']
}, {
category: 'meats',
food: ['chicken', 'turkey', 'beef']
}, {
category: 'fruits',
food: ['bannana', 'apple', 'pineapple']
}, {
category: 'canned goods',
food: ['tomatoes', 'green beans', 'corn']
}, {
category: 'veggies',
food: ['broccoli', 'celery', 'lettuce']
}, {
category: 'pantry',
food: ['broom', 'mop', 'dried beans']
}, ],
isHidden: true,
}
then I have my method to alter isHidden
viewItemsinCat: function(){
this.isHidden = false
},
Add a selected property to contain the currently selected item when the user clicks:
data() {
return {
selected: null,
list: [...]
}
}
<div v-for="item in list">
<h4 v-on:click="selected=item.category">{{item.category}}</h4>
<ul v-if="selected==item.category">
<li v-for="food in item.food">
{{ food }}
</li>
</ul>
</div>
Here's a demo

Generate a list with a head based on object property in array

In Vue, i'm trying to generate a list based on a property in an object.
I have an array coming from the vuex store that looks like this:
const array = [
{
name: "British title string"
nationality: "British"
},
{
name: "Another title"
nationality: "American"
},
{
name: "Name"
nationality: "Dutch"
},
{
name: "Another american item"
nationality: "American"
},
];
What i want to have is the output like this using v-for:
<h2>British</h2>
<ul>
<li>British title string</li>
</ul>
<h2>American</h2>
<ul>
<li>Another title</li>
<li>Another american item</li>
</ul>
<h2>Dutch</h2>
<ul>
<li>Name</li>
</ul>
I have already sorted the array by the nationality property using lodash _.sortBy and it's given me an array sorted by nationality but i want to add a H2 element with the value of nationality.
If you have multiple items with same nationality and you want to group them up, use _.groupBy() before _.sortBy():
Vue.config.devtools = false;
Vue.config.productionTip = false;
new Vue({
el: '#app',
template: '#appTemplate',
data: () => ({
rawData: [{
name: "British title string",
nationality: "British"
},
{
name: "Another title",
nationality: "American"
},
{
name: "Name",
nationality: "Dutch"
},
{
name: "Another american item",
nationality: "American"
}
]
}),
computed: {
groupedItems() {
return _.sortBy(
_.map(
_.groupBy(this.rawData, 'nationality'),
items => ({
items,
nationality: items[0].nationality
})
),
['nationality']
);
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
<script type="text/template" id="appTemplate">
<div>
<template v-for="group in groupedItems">
<h2 v-text="group.nationality" />
<ul>
<li v-for="(item, index) in group.items" :key="index" v-text="item.name" />
</ul>
</template>
</div>
</script>
<div id="app"></div>
For convenience and data readability, I mapped first item's nationality as the group's nationality (and named the grouped items items), but you could have used item 0's nationality in the template directly, instead.
To demonstrate, here's how that would have looked like:
Vue.config.devtools = false;
Vue.config.productionTip = false;
new Vue({
el: '#app',
template: '#appTemplate',
data: () => ({
rawData: [{
name: "British title string",
nationality: "British"
},
{
name: "Another title",
nationality: "American"
},
{
name: "Name",
nationality: "Dutch"
},
{
name: "Another american item",
nationality: "American"
}
]
}),
computed: {
groupedItems() {
return _.sortBy(
_.groupBy(this.rawData, 'nationality'),
['0.nationality']
);
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
<script type="text/template" id="appTemplate">
<div>
<template v-for="grouped in groupedItems">
<h2 v-text="grouped[0].nationality" />
<ul>
<li v-for="(item, index) in grouped" :key="index" v-text="item.name" />
</ul>
</template>
</div>
</script>
<div id="app"></div>
Note both of the above examples output the required markup. If you want each item to have its own wrapper, replace <template> with an html tag and add keys. i.e:
<article v-for="(grouped, key) in groupedItems" :key="key">
You can do this, considering that the array is ordered:
const array = [
{
name: "Another title",
nationality: "American"
},
{
name: "Another american item",
nationality: "American"
},
{
name: "British title string",
nationality: "British"
},
{
name: "Name",
nationality: "Dutch"
},
];
const arrayAux = [];
var j = 0;
for (var i = 0; i < array.length;) {
console.log(array[i].name);
arrayAux.push({
nationality: array[i].nationality,
names: [array[i].name]
});
i++;
while ( i < array.length && array[i].nationality === array[i - 1].nationality) {
arrayAux[j].names.push(array[i].name);
i++;
}
j++;
}
console.log(arrayAux);
In the html:
<div v-for="item in arrayAux">
<h2>{{ item.nationality }}</h2>
<ul>
<li v-for="n in item.names">
{{ n.name }}
</li>
</ul>
</div>

Knockout "with" binding

I am trying to display descendant elements in array using "with" binding.
But it displays only last items in "exercises" and I want to see all of them. How is it possible to fix this?
And after that, how can I make each item in array editable?
My ViewModel:
function AppViewModel() {
var self = this;
self.workouts = ko.observableArray([
{name: "Workout1", exercises:{
name: "Exercise1.1",
name: "Exercise1.2",
name: "Exercise1.3"
}},
{name: "Workout2", exercises:{
name: "Exercise2.1",
name: "Exercise2.2",
name: "Exercise2.3"
}},
{name: "Workout3", exercises:{
name: "Exercise3.1",
name: "Exercise3.2",
name: "Exercise3.3"
}},
{name: "Workout4", exercises:{
name: "Exercise3.1",
name: "Exercise3.2",
name: "Exercise3.3"
}},
]);
self.removeWorkout = function() {
self.workouts.remove(this);
};
}
ko.applyBindings(new AppViewModel());
The View:
<div class="content">
<ul data-bind="foreach: workouts">
<li>
<span data-bind="text: name"> </span>
Remove
<ul data-bind="with: exercises">
<li data-bind="text: name"></li>
</ul>
</li>
</ul>
</div>
Here's this code at jsfiddle:
http://jsfiddle.net/9TrbE/
Thanks!
The exercises property you declared as an object should be an array.
self.workouts = ko.observableArray([
{name: "Workout1", exercises:[
{ name: "Exercise1.1" },
{ name: "Exercise1.2" },
{ name: "Exercise1.3" }
]},
]);
So you can use this view :
<div class="content">
<ul data-bind="foreach: workouts">
<li>
<span data-bind="text: name"> </span>
Remove
<ul data-bind="foreach: exercises">
<li data-bind="text: name"></li>
</ul>
</li>
</ul>
</div>
Declaring :
var exercises = {
name: "Exercise1.1",
name: "Exercise1.2",
name: "Exercise1.3"
};
Is like doing that :
var exercises = {
name: "Exercise1.1",
};
exercises.name: "Exercise1.2";
exercises.name: "Exercise1.3";
Get it with this
Here's this code at jsfiddle
self.workouts = ko.observableArray([
{name: "Workout1", exercises:[
{ name: "Exercise1.1" },
{ name: "Exercise1.2" },
{ name: "Exercise1.3" }
]},
]);
`http://jsfiddle.net/9TrbE/8/

Categories

Resources