Render nested list in vue without nesting HTML elements? - javascript

I am in a bit of a problem cant think of way to solve neither could find anything relevant on google or rather I don't know what to search for.
I have an array categories and an object containing subcategories
categories: [
'Mobiles',
'Mobiles Accessories',
...
]
subcategories: {
'Mobiles': [
'Mi',
'Samsung',
'Infinix',
...
],
'Mobile Accessories': [
'Mobiles Cases',
'Headphones & Earphones',
...
]
}
Now i want render a menu using that in following way.
<span class="category">Mobiles</span>
<span class="subcategory">Mi</span>
<span class="subcategory">Samsung</span>
<span class="subcategory">Infinix</span>
...
<span class="category">Mobile Accessories</span>
<span class="subcategory">Mobiles Cases</span>
<span class="subcategory">Headphones & Earphones</span>
...
If i use v-for in vue i will have to nest .categories and .subcategories inside a parent element for each category, which i don't want to do.
This whole mess is that i can use flex-direction: column in the parent element and when rendering the list, it automatically flows to next column which wont happen if i nest .categories and .subcategories in a parent element as now each parent element for categories will move to next column instead of its height.
So how can i achieve this either by changing CSS or the data structure.
Thanks
Akash.

Note that in your question Mobiles Accessories does not equal Mobile Accessories so you have to make sure the categories array contains the exaxt names of the keys in subcategories.
You can use reduce and map:
const categories = ['Mobiles', 'Mobile Accessories'];
const subcategories = {
Mobiles: ['Mi', 'Samsung', 'Infinix'],
'Mobile Accessories': ['Mobiles Cases', 'Headphones & Earphones'],
};
console.log(
categories.reduce(
(result, value) =>
result.concat(
{ class: 'category', value },
subcategories[value].map((value) => ({
class: 'subcategory',
value,
})),
),
[],
),
);

Here is the Solution for You!!!
Using two for loops you can do this.
var app = new Vue({
el: '#app',
data: {
menu: {
categories:[
'Accessories',
'Mobiles',
],
subcategories: {
'Accessories': [
'Mobiles Cases',
'Headphones & Earphones',
],
'Mobiles': [
'Mi',
'Samsung',
'Infinix'
],
}
}
}
});
.category{
display:block;
padding:10px 0px;
}
.subcategory{
display:block:
margin:5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="(menus,index) in menu.categories">
<span class="category">{{ menus }}</span>
<span class="subcategory" v-for="(submenu,index) in menu.subcategories[menus]">{{submenu}}<br/></span>
</div>
</div>

Related

How to Store Data Property Value from a Specific Item in Rendered List in Vue

I'm trying create a follow button on list items in Vue. My strategy is to grab the value of a particular list item property and store it in the data object. Then use this value in a method to add it to an array in my database.
<div v-for="result in results" :key="result.symbol">
{{ result.name }}
<button #click="followStock">+follow</button>
</div>
I'm not sure how to get the value of result.symbol "into" the button element to set the value symbol in the data object below.
<script>
export default {
data() {
return {
results: [ // this is populated by an api call
{
currency: "USD"
exchangeShortName: "NYSE"
name: "International Game Technology PLC"
stockExchange: "NYSE"
symbol: "IGT"
},
{...},
...
],
symbol: "",
};
},
followStock() {
// add this.symbol to database array
},
},
};
</script>
I'm guessing there might be an easier strategy I'm overlooking as I'm still new to Vue, so any other solution that essentially allows me to fire off the value of result.symbol from any rendered result to my database would be awesome.
You can just pass the result as a parameter to your method.
<div v-for="result in results" :key="result.symbol">
{{ result.name }}
<button #click="followStock(result)">+follow</button>
</div>
And in your method:
methods: {
followStock(result) {
// do something with result
console.log({result});
let symbol = result.symbol;
},
}
P.S I didn't see you put your followStock() inside a methods object, but I did so in the example. https://v2.vuejs.org/v2/api/#methods
Write directly as a function call.
The vue compiler will turn followStock(result.symbol) into function(event) {followStock(result.symbol)}.
new Vue({
el: '#app',
data() {
return {
results: [
{
name: "International Game Technology PLC",
symbol: "IGT"
},
{
name: "A name",
symbol: "A symbol"
}
]
};
},
methods: {
followStock(symbol) {
console.log(symbol)
},
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="result in results" :key="result.symbol">
{{ result.name }}
<button #click="followStock(result.symbol)">+follow</button>
</div>
</div>
As Nazaire mentioned you can access the results anywhere inside the child elements when using v-for.
(it works like a normal for-loop)
It's not only limited to the corresponding element (the element in which you do v-for)
<div v-for="result in results" :key="result.symbol">
{{ result.name }}
<button #click="followStock(result.symbol)">+follow</button>
</div>
followStock(symbol){
// you can now add symbol to db
}

Deleting specific component in v-for array

I have below array, that contains a number of columns. Below example contains three columns, but columns can be added/removed dynamically:
[['position', '30'], ['position', '60'], ['position', '90']]
I am facing issues when deleting the correct column (index in array) with Vue.
Consider below snippet:
new Vue({
el: '#app',
data: {
message: 'Hello Vue.js!',
columns: [['position', '30'], ['position', '60'], ['position', '90']]
},
methods: {
deleteColumn: function(index) {
this.columns.splice(index, 1);
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="(item, index) in columns" :key="index">
Column #: {{index}} - <a #click="deleteColumn(index)">Delete me</a>
</div>
</div>
If you run the above code snippet end try to delete the #1 column, it will actually remove the #2 column (last item of the array). Same goes for #0.
I thought that by providing the index to my deleteColumn function, I could remove the "right" index from the array.
Any help is appreciated.
Just give them a property name and you are done. Notice what I changed here. Columns is no more a 2D array, but objects. Use this.$delete(this.columns, index); to delete the objects.
new Vue({
el: '#app',
data: {
message: 'Hello Vue.js!',
columns: {
'1': {
position: 30
},
'2': {
position: 60
},
'3': {
position: 90
}
}
},
methods: {
deleteColumn: function(index) {
this.$delete(this.columns, index);
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="(item, index) in columns" :key="index">
Column #: {{index}} - <a #click="deleteColumn(index)">Delete me</a>
</div>
</div>
{
'1': {
position: 30
},
'2': {
position: 60
},
'3': {
position: 90
}
}
Here, '1' is a property name and it's value is another object. It's like giving ids to your data.
The format for value of object is this
{ property_name : value }
Here, value is another object, and in that object, there is another property, named "position" with your corresponding values.
When you clicked any item you are removing it in the right way, your index is your key, that's the problem, but is visually, in the logic it's right. Display your position in your template just for you can see it. ANd for me your data it's not in the right way.
<div id="app">
<div v-for="(item, index) in columns" :key="index">
Column #: {{index}}-{{item.position}} -
<a #click="deleteColumn(index)">Delete me</a>
</div>
</div>
and your script for you can see the change
new Vue({
el: '#app',
data: {
message: 'Hello Vue.js!',
columns: [{position: 30}, {position: 60}, {position: 90}]
},
methods: {
deleteColumn: function(index) {
this.columns.splice(index, 1);
}
}
})
The splice method reindexes the array, moving all elements after the splice point up or down so that any new inserted values will fit and so that the array indices remain contiguous. You can see it more clearly if you also display the values of the items in your list:
new Vue({
el: '#app',
data: {
message: 'Hello Vue.js!',
columns: ['foo', 'bar', 'baz']
},
methods: {
deleteColumn: function(index) {
this.columns.splice(index, 1);
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="(item, index) in columns" :key="index">
Column #{{index}} = {{item}} - <a #click="deleteColumn(index)" style="cursor:pointer">Delete me</a>
</div>
</div>
Initially, the snippet above will render like this:
Column #0 = foo - Delete me
Column #1 = bar - Delete me
Column #2 = baz - Delete me
If you now click the "Delete me" link on column #0 ("foo"), it will change to:
Column #0 = bar - Delete me
Column #1 = baz - Delete me
You can see that the value "foo" indeed got spliced out of the array — and the values "bar" and "baz" were shifted down by one position to become the new elements #0 and #1.
Anyway, the fix for this problem is simply "don't do that":
If you're using v-for with a simple array whose elements have no natural key value, you can just omit :key entirely and let Vue decide how to best handle changes to the underlying array. As long as the contents within the v-for loop doesn't contain any form inputs or stateful components or other fancy stuff that doesn't react well to the array being reindexed, it should work just fine.
Conversely, if you do have a natural unique key available for each array element, use it. If you don't, but can create one, consider doing that.
You should not use index as the key with CRUD operations since this will confuse Vue when it comes to deleting. The key should be a unique identifier that relates to the data.
You can create a new formatted array of objects on mount with a key generated from the data within the array (note: I haven't tested the code in a browser if there are any mistakes).
<template>
<div>
<div v-for="col in formattedColumns" :key="col.key">
{{ col.value }}
</div>
</div>
</template>
<script>
export default {
data() {
return {
columns: [['position', '30'], ['position', '60'], ['position', '90']],
formattedColumns: null,
};
},
mounted() {
let columns = [];
for (let i = 0; i < this.columns.length; i++) {
columns.push({
value: this.columns[i],
key: this.columns[i][0] + this.columns[i][1],
});
}
this.formattedColumns = columns;
},
};
</script>
Try this this.$delete(this.columns, index) which is the same as Vue.delete(this.columns, index)
https://v2.vuejs.org/v2/api/index.html#Vue-delete

Angular: push data into nested array

I trying to add sub menu by clicking the button but it doesn't work. My data looks like:
$scope.menuItems = [
{ name: 'Menu1',
children: [
{ name: 'Sub1' },
{ name: 'Sub2'} ]
},
{ name: 'Menu1',
children: [
{ name: 'Sub1' } ]
}
];
$scope.addSubItem = function() {
$scope.menuItems.children.push({
name: 'Test Sub Item'
});
};
http://plnkr.co/edit/2R5kpY2iGhiE6FEy65Ji?p=preview
Plunker Solution here
You need to modify the submenu button markup to pass the reference to the menu item that the button resides in:
<ul class="sub-menu">
<li ng-repeat="menuItem in menuItem.children" ng-include="'sub-tree-renderer.html'"></li>
<button class="btn btn-warning" style="margin-top: 10px;" ng-click="addSubItem(menuItem)">Add Sub Menu Item</button>
</ul>
and then in your addSubItem function operate directly on the item like this:
$scope.addSubItem = function(item) {
item.children.push({
name: 'Sub' + (item.children.length + 1)
});
};
Also make sure that every time you create new item the children array is defined as empty array instead of being undefined:
$scope.addItem = function() {
$scope.menuItems.push({
name: 'Test Menu Item',
children: []
});
};
I would recommend using data value object that you can construct a new item with instead of using hand typed object literals as if you use them in many places it is easy to make mistake and cause bugs which happens only in some places and are time consuming to find.
You need to specify the index of the menuItems array that you wish to add the sub menu to.
This would add a sub menu to the first menu item:
$scope.menuItems[0].children.push({
name: 'Test Sub Item'
});
Also, if this is of n depth and can vary depending on the data that is driving the menu, you could build a controller for the menu item and have it recursively add a child/show in your template based on the node you are clicking on. Then you don't need to explicitly worry about indexes.
firstly you should determine sub menu by it index. here you can use $index for this. when you add new Item just add item name. when you need add children array also.
<ul class="sub-menu">
<li ng-repeat="menuItem in menuItem.children" ng-include="'sub-tree-renderer.html'"></li>
<button class="btn btn-warning" style="margin-top: 10px;" ng-click="addSubItem($index)">Add Sub Menu Item</button>
</ul>
and in controller
$scope.addSubItem = function(index) {
$scope.menuItems[index].children.push({
name: 'Test Sub Item'
});
};
$scope.addItem = function() {
var item = {
name: 'Test Menu Item',
children: []
};
$scope.menuItems.push(item);
};

How to set the selected value for multiple select elements in a single AngularJS context

I try to create multiple select elements on a AngularJS page in a single context. names is a array of available name objects and cores is the array with available cores. For each name, I would like to have a drop down with the available cores. This works fine, but I'm not able to pre-select the currently selected core. That one is stored in name.core.
<ul>
<li ng-repeat='name in names'>
{{ name.name }} - {{ name._url }} <span ng-click='delete_name(name)'>[x]</span>
<select ng-model='?'
ng-options='core.id as core.instance for core in cores'
ng-change='coreChanged(name, ?)'>
<option value=''>No core assigned</option>
</select>
</li>
</ul>
I'm quite new to AngularJS, so if another structure or additional controllers, scopes, ... would be more appropriate, I'm open for suggestions.
Update:
ng-model='name.core' as proposed by haki does not work, probably because name.core is not an instance from the cores array. I tried ng-model='name.core.id' but that results in every drop down having the same (wrong) selection.
This works fine for me:
<ul>
<li ng-repeat='name in names'>
{{ name.name }} - {{ name._url }} <span ng-click='delete_name(name)'>[x]</span>
<select ng-model='name.core'
ng-options='core.id as core.instance for core in cores'
ng-change='coreChanged(name)'>
<option value=''>No core assigned</option>
</select>
</li>
</ul>
Double-check on how your view corresponds to your model. My scope variables look like this:
.controller('MyController', function($scope){
$scope.names = [
{ name: 'Marc', url: 'http://www.example.com/one', core: "1" },
{ name: 'Achim', url: 'http://www.example.com/two', core: "2" },
{ name: 'Jenny', url: 'http://www.example.com/three', core: "3" }
];
$scope.cores = [
{ id: '1', instance: 'First' },
{ id: '2', instance: 'Second' },
{ id: '3', instance: 'Third' }
];
$scope.coreChanged = function(name){
console.log(name);
}
});
Working Plunker

Knockout mapping - Uniquely identifying nested objects using keys

The Knockout mapping plugin documentation has a section entitled "Uniquely identifying objects using “keys”". This describes how to update part of an object and then only update that part of the display rather than completely replacing the display of all properties of a partially-modified object. That all works splendidly in their simple example, which I have slightly modified here to make my question more clear. My modifications were to:
Replace the object with a corrected name after a 2 second delay.
Highlight the unchanging part of the display, so you can see that it is actually not replaced when the update happens.
1. Simple object (jsFiddle)
<h1 data-bind="text: name"></h1>
<ul data-bind="foreach: children">
<li><span class="id" data-bind="text: id"></span> <span data-bind="text: name"></span></li>
</ul>
<script>
var data = {
name: 'Scot',
children: [
{id : 1, name : 'Alicw'}
]
};
var mapping = {
children: {
key: function(data) {
console.log(data);
return ko.utils.unwrapObservable(data.id);
}
}
};
var viewModel = ko.mapping.fromJS(data, mapping);
ko.applyBindings(viewModel);
var range = document.createRange();
range.selectNode(document.getElementsByClassName("id")[0]);
window.getSelection().addRange(range);
setTimeout(function () {
var data = {
name: 'Scott',
children: [
{id : 1, name : 'Alice'}
]
};
ko.mapping.fromJS(data, viewModel);
}, 2000);
</script>
But what isn't clear to me is how I would achieve the same behavior for a more complex nested data structure. In the following example, I took the above code and wrapped the data in a list. I would like this to behave the same as above, but it doesn't. The whole display is redone because of the change in one property. You can see this because, unlike the above example, the highlighting is lost after the data is updated.
2. More complex nested object (jsFiddle)
<!-- ko foreach: parents -->
<h1 data-bind="text: name"></h1>
<ul data-bind="foreach: children">
<li><span class="id" data-bind="text: id"></span> <span data-bind="text: name"></span></li>
</ul>
<!-- /ko -->
<script>
var data = {
parents: [
{
name: 'Scot',
children: [
{id : 1, name : 'Alicw'}
]
}
]
};
var mapping = {
children: {
key: function(data) {
console.log(data);
return ko.utils.unwrapObservable(data.id);
}
}
};
var viewModel = ko.mapping.fromJS(data, mapping);
ko.applyBindings(viewModel);
var range = document.createRange();
range.selectNode(document.getElementsByClassName("id")[0]);
window.getSelection().addRange(range);
setTimeout(function () {
var data = {
parents: [
{
name: 'Scott',
children: [
{id : 1, name : 'Alice'}
]
}
]
};
ko.mapping.fromJS(data, viewModel);
}, 2000);
</script>
So basically what I'm asking is, how can I make the second example work like the first, given the more nested data structure? You can assume that ids are unique for each child (so if I added another parent besides Scott, his children would start with id=2, etc.).
Interesting observation there and nice write-up. It appears to work if you define a key on the parent as well as the child. Try this fiddle:
http://jsfiddle.net/8QJe7/6/
It defines instantiable view model functions for the parents and children, where the parent constructor does its child mappings.

Categories

Resources