Angular: push data into nested array - javascript

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);
};

Related

Render nested list in vue without nesting HTML elements?

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>

How to append children to object dynamically

Suppose I have a object like as shown below:
var ob = [
{name: "root",id: 1},
{name: "root2",id: 2}
];
And I want to append children object dynamically to it. For example:
Suppose if I click on id 1 then children object should be appended to ob object.
var ob = [
{name: "root",id: 1, children: [
{name: 'sub1', id:'5'},
{name: 'sub2', id:'6'},
]
},
{name: "root2",id: 2}
];
Now if I click again on id 6 again children should be added to id 6.
var ob = [
{name: "root",id: 1, children: [
{name: 'sub1', id:'5'},
{name: 'sub2', id:'6', children: [
{name: 'subsub1', id:'8'},
{name: 'subsub2', id:'9'},
]
},
]
},
{name: "root2",id: 2}
];
I am trying to write a recursive function for it but no success. On click of any term I have reference only to the clicked term. I don't know about the parent term.
EDIT:
Below is my code:
<div *ngFor = "let term of terms">
<div class="row tr">
<a (click) = "showTerms($event)">{{term.id}}</a>
</div>
<div class="col-xs-6">{{term.desc}}</div>
<app-icd-codes *ngIf = "term.children" [terms] = "term.children"></app-icd-codes>
</div>
Here on click of a tag I am adding children's. So I need to create a dynamic object and update that object as shown above.
The most easy way is pass as argument the index of "terms". Put two buttons, one to AddTerms and another one to hideTerms/showTerms.
<div *ngFor = "let term of terms;let i=index">
<!--see the way to get the index of the array -->
<div class="row tr">
{{term.id}}
<!--you make a link, I use a button-->
<!--the button "add" is visible when there're NOT "children"-->
<button *ngIf="!term.terms" (click)="addTerms(i)">Add</button>
<!--the button to Show/hide is visible when there ARE "children"-->
<button *ngIf="term.terms" (click)="term.show=!term.show">
<span *ngIf="term.show">^</span>
<span *ngIf="!term.show">v</span>
</button>
</div>
<ng-container *ngIf ="term.terms && term.show">
<app-icd-codes [terms]="term.terms"></app-icd-codes>
</ng-container>
</div>
Then you must put your function addTerms. A simple function can be like
//see that you received the "index" of children
addTerms(index: number) {
this.terms[index].show = true; //<--to show the children
this.terms[index].terms = [{ id: 3 }, { id: 4 }]; //a "easy" way to add
}
Ok, really the function must be like
addTerms(index: number) {
let id=this.terms[index].id; //we get the "id"
//call a httpClient and subscribe
this.httpClient.get("url/"+id).subscribe(res=>
//in the subscription
{
this.terms[index].show = true; //<--to show the children
this.terms[index].terms = res
})
}
NOTE: Can result "strange" add new properties to an Object (in this case "children" and "show"). If we feel more confortable, we can add the properies when we create the object with a null value

Checkbox in AngularJS "double checking" after click

I'm trying to make a simple to do list app using Angular. When the user clicks the checkbox, the list item should disappear. Right now, the list item disappears upon clicking the checkbox, but the checkbox of the list item directly below it also gets checked (although the list item doesn't disappear).
<ul ng-repeat="item in arr track by $index">
<li>
<input type="checkbox" ng-click="check(item)">
<span>{{ item }}</span>
</li>
</ul>
$scope.arr = [
'todo 1',
'todo 2',
'todo 3'
];
$scope.check = function(item) {
var indexOf = $scope.arr.indexOf(item);
if (indexOf !== -1) {
$scope.arr.splice(indexOf, 1);
}
};
Is there any way to solve this?
Not sure how this is working as-is, but the normal way to accomplish this is with an ng-model on the <input>. Make your items into objects so they can store more complex state, which also lets you drop track by.
<ul ng-repeat="item in arr">
<li>
<input type="checkbox" ng-model="item.done">
<span>{{item.label}}</span>
</li>
</ul>
$scope.arr = [
{label: 'todo 1', done: false},
{label: 'todo 2', done: false},
{label: 'todo 3', done: false}
];
That should get your checking and unchecking set up correctly. To drop items when they're done, I recommend a filer on the ng-repeat.
ng-repeat="item in arr | filter: {done: 'false'}"
https://plnkr.co/edit/ybikvwysC6ZEd7CeZnnt?p=preview
The checkbox directly under the deleted item was automatically being checked because the information, that first the element in array is checked was still valid, but the length of the array reduces with every splice method executed. That's why you have to actually tell angular that check another element, which takes index: 0 in the array after previous element gets spliced is unnecessary.
Here's the solution: Plunker link
Raw code:
<ul ng-repeat="item in arr track by $index">
<li>
<input type="checkbox" ng-click="check(item)" ng-model="item.selected">
<span>{{ item }}</span>
</li>
</ul>
$scope.arr = [
'todo 1',
'todo 2',
'todo 3'
];
$scope.check = function(item) {
var indexOf = $scope.arr.indexOf(item);
if (indexOf !== -1) {
$scope.arr.splice(indexOf, 1);
$scope.item.selected = false;
}
That happens because you use "track by $index". Try without track by, or use track by with some unique field (id), in which case your list should consist of objects, not primitives. For example:
$scope.arr = [
{id: 1, name: 'todo 1'},
{id: 2, name: 'todo 2'},
{id: 3, name: 'todo 3'} //...
];
<ul ng-repeat="item in arr track by item.id">
<li>
<input type="checkbox" ng-click="check($index)">
<span>{{item.name}}</span>
</li>
</ul>
$scope.check = function(index) {
$scope.arr.splice(index, 1);
}
You need to prevent the click event from performing the default action
<input type="checkbox" ng-click="check(item);$event.preventDefault();" />
Angular is removing the element, on the click, but then the (moved) element also gets the click event, checking it. $event.preventDefault() will prevent this.
Here is a working plunk

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