I'm having an issue creating an object in the correct format when adding to it from a loop.
e.g. I loop through some lists
<ul class="pdmenu">
<ul class="pdmenu">
<li class="menu-top" id="vmen-1">1</li>
<li class="menu-item">aaa</li>
</ul>
<ul class="pdmenu">
<li class="menu-top" id="vmen-2">2</li>
<li class="menu-item">aaa</li>
<li class="menu-item">bbb</li>
</ul>
<ul class="pdmenu">
<li class="menu-top" id="vmen-3">3</li>
<li class="menu-item">aaa</li>
<li class="menu-item">bbb</li>
</ul>
I use jQuery to loop through the top list item, .menu-top, and push the id and visibility to an object.
jsonmenu = $(); // Set empty object.
$('.menu-top').each(function(index) {
jsonmenu.push({
menu: $(this).attr('id'),
visible: "" + $(this).next().is(':visible') + ""
});
});
This creates the object with member for each item like so
{
"0": {
"menu":"vmen-1",
"visible":"false"
},
"length":4,
"1": {
"menu":"vmen-2",
"visible":"false"
},
"2":{
"menu":"vmen-3",
"visible":"false"
},
"3": {
"menu":"vmen-4",
"visible":"true"
}
}
All I need is a simple format like the following;
{
"menu":"vmen-1",
"visible":"false"
},
{
"menu":"vmen-2",
"visible":"false"
},
{
"menu":"vmen-3",
"visible":"false"
},
{
"menu":"vmen-4",
"visible":"true"
}
How can I change this to get the object in this simple format?
Use a native array and not a JSON object to hold your values:
var jsonmenu = [];
$('.menu-top').each(function(index) {
jsonmenu.push({menu: $(this).attr('id'), visible: ""+$(this).next().is(':visible')});
});
Or, as you just got something like key-value pairs, you could do it like this:
var jsonmenu = {};
$('.menu-top').each(function(index) {
jsonmenu[ $(this).attr('id') ] = $(this).next().is(':visible');
});
which would result in something like this:
{
'vmen-1': false,
'vmen-2': false,
...
}
You've already had an answer, to use a native array rather than an empty jQuery object. However, for completeness, there is another convenient method in jQuery to do things like "enumerate a bunch of DOM elements and turn them into an array" .map()
You would use it like so:
var jsonmenu = $('.menu-top').map(function(i,e) {
var $this = $(e);
return {
id: $this.attr('id'),
visible: "" + $this.next().is(':visible') + ""
}
});
Live example: http://jsfiddle.net/8szrG/
Related
I used the foreach method to create markup foreach item in an observable array to create a treeview.
output example
category name1
content
content
category name 2
content
content
when I click on the category name I want just its content to show/hide, currently when I click on the category name it shows and hides all the categories.
var reportFilters = [
{ Text: "Campaign", Value: primaryCategories.Campaign },
{ Text: "Team", Value: primaryCategories.Team },
{ Text: "Agent", Value: primaryCategories.Agent },
{ Text: "List", Value: primaryCategories.List },
{ Text: "Inbound", Value: primaryCategories.Inbound },
{ Text: "Daily", Value: primaryCategories.Daily },
{ Text: "Services", Value: primaryCategories.Services },
{ Text: "Occupancy", Value: primaryCategories.Occupancy },
{ Text: "Data", Value: primaryCategories.Data }
];
self.showCategory = ko.observable(false);
self.toggleVisibility = function (report) {
var categoryName = report.PrimaryReportCategory;
var categoryContent = report.ID;
if (categoryName == categoryContent ) {
self.showCategory(!self.showCategory());
};
}
<div class="report-category-treeview" data-bind="foreach: $root.categories, mCustomScrollBar:true">
<ul class="column-list" >
<li class="report-category-heading" data-bind="click: $root.toggleVisibility"><span class="margin-top10" ><i class="fas fa-chevron-down"></i> <span class="report-category-name" data-bind="text: categoryName"></span></span></li>
<li id="panel" class="report-category-container" data-bind="foreach: reports, visible: $root.showCategory">
<div class="column-list-item" data-bind="click: $root.report_click, css: { 'selected': typeof $root.selectedReport() != 'undefined' && $data == $root.selectedReport() }">
<span class="column-list-text" data-bind="text: ReportName"></span>
</div>
</li>
</ul>
</div>
currently, when I click on the category name, it shows and hides all the
categories.
It's because showCategory is your single observable responsible for showing\hiding. What you really want is one show\hide observable per category.
I'm not sure how your entire data model looks like, but since you specifically asked about categories, then you should create a category view model, and probably some container view model, which I'll name here master:
var categoryVM = function (name) {
var self = this;
self.name = ko.observable(name);
self.isVisible = ko.observable(false);
self.toggleVisibility = function () {
self.isVisible(!self.isVisible());
}
// ... add here your other observables ...
}
// name 'masterVM' whatever you like
var masterVM = function () {
var self = this;
self.categories = ko.observables([]);
// ... probably add here other observables, e.g. 'reports' ...
self.init = function (rawCategories) {
rawCategories.forEach(function (item) {
categories.push(new categoryVM(item.name)); // replace 'name' with your property
}
}
}
var master = new masterVM();
master.init(getCategories()); // pass in your categories from wherever they come from
ko.applyBindings(master);
Then, in your html, this would be your outer foreach:
<div class="report-category-treeview" data-bind="foreach: categories ... />
and your lis (for brevity, I'm ommiting nested tags under your lis):
<li class="report-category-heading"
data-bind="click: toggleVisibility">
<li id="panel" class="report-category-container"
data-bind="foreach: $root.reports, visible: isVisible">
I have a json:
{"sectionTitle":"Account Information","sectionItems":[{"itemTitle":"Balance","url":"/account/balance","selected":true},{"itemTitle":"Account Statement","url":"/account/statementsearch","selected":false},{"itemTitle":"Deposit","url":"/account/deposit","selected":false},{"itemTitle":"Withdrawal","url":"/account/withdraw","selected":false},{"itemTitle":"Edit Profile","url":"/account/editprofile","selected":false},{"itemTitle":"Change Password","url":"/account/changepassword","selected":false}]}
Now I just want to check if there is an item (child) inside sectionTitle where selected is true.
Something like this in SQL
SELECT * FROM sectionItems WHERE selected=true
Can I do something similar in angular js, so I can check if the the parents has children?
I hope you understood my question.
This is my html
<nav class="sidebar-nav">
<ul class="nav metismenu" id="side-menu-help">
<li ng-repeat="menuItem in accountCtrl.menuStructure">
<a class="{{ (menuItem.sectionItems.length > 0) ? 'metisHasChildren' : '' }}" href="/en/help-area/poker-help/poker-rules/">
<span ng-if="menuItem.sectionItems.length > 0" class="fa arrow fa fa-angle-double-down"></span>
{{ ::menuItem.sectionTitle }}
{{ ::menuItem }}
</a>
<ul class="nav nav-second-level collapse in">
<li ng-repeat="subMenuItem in menuItem.sectionItems" ng-click="accountCtrl.changePage(subMenuItem.url)">
<a ng-class="(subMenuItem.selected) ? 'page-active' : ''">{{ ::subMenuItem.itemTitle }}</a>
</li>
</ul>
</li>
</ul>
</nav>
You do not need to anything fancy to get this working. Simply convert the json to an object and access the property you want using dot notation. So for example:
var json = JSON.parse(json);
var selectedItems = [];
angular.forEach(json.sectionItems, function(sectionItem) {
if (sectionItem.selected) {
selectedItems.push(sectionItem);
}
});
Would convert the json string to an object and then loop over each sectionItem child, check for selected is true and create an array of matching items.
You can use a forEach loop. This example will return an array of all sectionItems where selected equals true, but you can return whatever you'd like.
$scope.items =[];
angular.forEach(sectionItems, function(item){
if (item.selected === true){
$scope.items.push(item);
}
})
UPDATE
Here's a working plunk
To make this work inline with ng-repeat you will use it in a filter, like this:
app.filter('menuFilter', function() {
return function(menuItems) {
var filtered = [];
angular.forEach(menuItems, function(menuItem) {
angular.forEach(menuItem.sectionItems, function(item) {
if (item.selected === true) {
filtered.push(menuItem);
}
});
});
return filtered;
}
});
And change your markup, like this:
ng-repeat="menuItem in accountCtrl.menuStructure | menuFilter "
First, you don't need to do any custom filter or anything like that, just use the standard filter, as below:
<li ng-repeat="menuItem in accountCtrl.menuStructure | filter: { sectionItems: { selected: true } }"> {{ menuItem.sectionTitle }}
Working demo:
angular.module('app', [])
.controller('accountController', function() {
var vm = this;
vm.menuStructure = [
{
"sectionTitle":"Account Information",
"sectionItems":[
{
"itemTitle":"Balance",
"url":"/account/balance",
"selected":true
},
{
"itemTitle":"Account Statement",
"url":"/account/statementsearch",
"selected":false
},
{
"itemTitle":"Deposit",
"url":"/account/deposit",
"selected":false
},
{
"itemTitle":"Withdrawal",
"url":"/account/withdraw",
"selected":false
},
{
"itemTitle":"Edit Profile",
"url":"/account/editprofile",
"selected":false
},
{
"itemTitle":"Change Password",
"url":"/account/changepassword",
"selected":false
}
]
},
{
"sectionTitle":"Account Information 2",
"sectionItems":[
{
"itemTitle":"Balance",
"url":"/account/balance",
"selected":false
},
{
"itemTitle":"Account Statement",
"url":"/account/statementsearch",
"selected":false
},
{
"itemTitle":"Deposit",
"url":"/account/deposit",
"selected":false
},
{
"itemTitle":"Withdrawal",
"url":"/account/withdraw",
"selected":false
},
{
"itemTitle":"Edit Profile",
"url":"/account/editprofile",
"selected":false
},
{
"itemTitle":"Change Password",
"url":"/account/changepassword",
"selected":false
}
]
}
];
});
<!DOCTYPE html>
<html ng-app="app">
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.7/angular.min.js"></script>
</head>
<body ng-controller="accountController as accountCtrl">
<ul>
<li ng-repeat="menuItem in accountCtrl.menuStructure | filter: { sectionItems: { selected: true } }"> {{ menuItem.sectionTitle }}
<ul class="nav nav-second-level collapse in">
<li ng-repeat="subMenuItem in menuItem.sectionItems" ng-click="accountCtrl.changePage(subMenuItem.url)">
<a ng-class="{ 'page-active': subMenuItem.selected }" ng-bind="::subMenuItem.itemTitle"></a>
</li>
</ul>
</li>
</ul>
</body>
</html>
Tips:
Instead of using ngClass with ternary operator, you can simply use this way:
ng-class="{ 'page-active': subMenuItem.selected }"
Even if works in this way that you're using, I'd recommend you to take a look on the special-repeats, it fits really well in your situation.
I hope it helps!!
var JSONStr=[{"sectionTitle":"Account Information","sectionItems":[{"itemTitle":"Balance","url":"/account/balance","selected":true},{"itemTitle":"Account Statement","url":"/account/statementsearch","selected":false},{"itemTitle":"Deposit","url":"/account/deposit","selected":false},{"itemTitle":"Withdrawal","url":"/account/withdraw","selected":false},{"itemTitle":"Edit Profile","url":"/account/editprofile","selected":false},{"itemTitle":"Change Password","url":"/account/changepassword","selected":false}]}];
var result = JSONStr.where({ selected: true });
I have a a collection of panels each with a simple list of items that needs to either be sorted by 'computedLoad' or 'Name'. I have the following objects and methods to accomplish this generically over all of the panels (only showing one panel among many).
scope.orderBy = {
name: {
displayName: "Name",
sort: "Name",
reverse: false
},
load: {
displayName: "Load",
sort: "-computedLoad",
reverse:false
}
};
scope.selectOrder = function (panel, order) {
timeout(function () {
panel.activeOrder = order;
});
};
scope.panels = {
methods: {
activeOrder: scope.orderBy.name
}
};
I have the following html:
<div>
<ul class="nav nav-pills">
<li class="list-label"><a>Order By:</a></li>
<li ng-repeat="order in orderBy">{{order.displayName}}</li>
</ul>
<ul class="nav nav-pills nav-stacked">
<li ng-repeat="item in suite.Methods | orderBy:panel.methods.activeOrder.sort"><span class="text">{{item.Name}}</span></li>
</ul>
</div>
The selectOrder method doesn't seem to work. Any ideas? Am I missing something?
Here is an example: http://jsbin.com/puxoxi/1/
Setting panel.activeOrder happens asynchronously, so it is outside of angulars so called "digest cycle".
To make angular re-evaluate your scope, use the $apply function:
It could look like this:
scope.$apply(function() {
panel.activeOrder = order;
});
Ok this may be a simple loop question that I'm just overthinking (would be far from the first time) but I'm going to post this just incase someone has a quick answer for me.
I have an array of DOM elements like this:
<ul id="array">
<li class='item' data-id="1">
<ul class="child">
<li class='item' data-id="2"></li>
<li class='item' data-id="3"></li>
</ul>
</li>
<li class='item' data-id="4"></li>
<li class='item' data-id="5">
<ul class="child">
<li class='item' data-id="6"></li>
<li class='item' data-id="7">
<ul class="child">
<li class='item' data-id="8"></li>
<li class='item' data-id="9"></li>
...
</ul>
</li>
</ul>
</li>
</ul>
I want to loop through that using jQuery and come out with something like:
var result = [
{
"id":1,
"children":[
{
"id":2,
"children":[]
},
{
"id":3,
"children":[]
}
]
},
{
"id": 4,
"children":[]
},
{
"id": 5,
"children":[
{
"id":6,
"children":[]
},
{
"id":7,
"children":[
{
"id":8,
"children":[]
},
{
"id":9,
"children":[]
}
]
}
]
}
]
(Note that is obviously spaced out for easy reading :) )
If that was the exact number of child ul items there would be, that would be an easy loop. The issue I'm having is that the list item tree could go as many levels down as a user wants (realistically, it will stop at some point but you get the idea) so it will need to continue to loop through the tree until there are no more.
You'll need a recursive function and map() method for this:
function extract() {
var $this = $(this);
return {
id: $this.data('id'),
children: $this
.find('> .child > .item')
.map(extract)
.get()
};
}
var output = $('#array > .item')
.map(extract)
.get();
Demo: http://jsfiddle.net/pj2C2/1/
This worked for me:
function arrayRecursive($arr) {
var result = [];
// with > .item you go only 1 level deeper per step
$arr.find('> .item').each(function(index, item) {
var obj = {};
obj.id = $(item).attr('data-id');
obj.children = arrayRecursive($(item).find('.child'));
result.push(obj);
});
return result;
}
var result = arrayRecursive($('#array'));
// Test the result with a stringified alert
alert(JSON.stringify(result));
EDIT:
Removed the if ($(item).find('.child').length > 0) so you have empty arrays as default.
Sounds like a recursion problem.
So you can loop through the li elements check to see if the child is a ul element if so another level of recursion else store the value at that level.
Some rough pseudo code
readElements($("#array").children());
function readElements(array) {
var elementsArray = [];
for (var i = 0; i < array.length; i++) {
if ($($(array[i]).children()[0]).is("ul")) {
return readElements($(array[i]).children());
}
elementsArray.push({ id: getId(), children: [] });
// add id elementArrays
}
return elementsArray;
}
I have an array of menu items, each containing Name and URL like this:
var menuItems = [
{
name : "Store",
url : "/store"
},
{
name : "Travel",
url : "/store/travel"
},
{
name : "Gardening",
url : "/store/gardening"
},
{
name : "Healthy Eating",
url : "/store/healthy-eating"
},
{
name : "Cook Books",
url : "/store/healthy-eating/cook-books"
},
{
name : "Single Meal Gifts",
url : "/store/healthy-eating/single-meal-gifts"
},
{
name : "Outdoor Recreation",
url : "/store/outdoor-recreation"
},
{
name : "Hiking",
url : "/store/outdoor-recreation/hiking"
},
{
name : "Snowshoeing",
url : "/store/outdoor-recreation/hiking/snowshoeing"
},
{
name : "Skiing",
url : "/store/outdoor-recreation/skiing"
},
{
name : "Physical Fitness",
url : "/store/physical-fitness"
},
{
name : "Provident Living",
url : "/store/provident-living"
}
]
I've been trying with no success to render this as an unordered list with a nested UL structure that follows the URL path structure like so:
<ul>
<li>Store
<ul>
<li>Travel</li>
<li>Gardening</li>
<li>Healthy Eating
<ul>
<li>Cook Books</li>
<li>Single Meal Gifts</li>
</ul>
</li>
<li>Outdoor Recreation
<ul>
<li>Hiking
<ul>
<li>Snowshoeing</li>
</ul>
</li>
<li>Skiing</li>
</ul>
</li>
<li>Physical Fitness</li>
<li>Provident Living</li>
</ul>
</li>
</ul>
All of the examples I've seen begin with a data structure that reflects the parent-child relationship (e.g. xml or JSON), but I'm having a very difficult time pulling this out of the URL and using it to render the new structure.
If anyone could please steer me in the right direction for how to do this using jQuery, I'd really appreciate it. I realize I probably need to use some recursive functions or maybe jQuery templates, but these things are still a bit new to me.
Thanks
I think the best solution is firstly to convert your data structure to a tree one, with parent/children relations. Render this structure will then be easier, as the UL itself has a tree structure.
You can convert menuItems using these couple of functions
// Add an item node in the tree, at the right position
function addToTree( node, treeNodes ) {
// Check if the item node should inserted in a subnode
for ( var i=0; i<treeNodes.length; i++ ) {
var treeNode = treeNodes[i];
// "/store/travel".indexOf( '/store/' )
if ( node.url.indexOf( treeNode.url + '/' ) == 0 ) {
addToTree( node, treeNode.children );
// Item node was added, we can quit
return;
}
}
// Item node was not added to a subnode, so it's a sibling of these treeNodes
treeNodes.push({
name: node.name,
url: node.url,
children: []
});
}
//Create the item tree starting from menuItems
function createTree( nodes ) {
var tree = [];
for ( var i=0; i<nodes.length; i++ ) {
var node = nodes[i];
addToTree( node, tree );
}
return tree;
}
var menuItemsTree = createTree( menuItems );
console.log( menuItemsTree );
The resulting menuItemsTree will be an object like this
[
{
"name":"Store",
"url":"/store",
"children":[
{
"name":"Travel",
"url":"/store/travel",
"children":[
]
},
{
"name":"Gardening",
"url":"/store/gardening",
"children":[
]
},
{
"name":"Healthy Eating",
"url":"/store/healthy-eating",
"children":[
{
"name":"Cook Books",
"url":"/store/healthy-eating/cook-books",
"children":[
]
},
{
"name":"Single Meal Gifts",
"url":"/store/healthy-eating/single-meal-gifts",
"children":[
]
}
]
},
{
"name":"Outdoor Recreation",
"url":"/store/outdoor-recreation",
"children":[
{
"name":"Hiking",
"url":"/store/outdoor-recreation/hiking",
"children":[
{
"name":"Snowshoeing",
"url":"/store/outdoor-recreation/hiking/snowshoeing",
"children":[
]
}
]
},
{
"name":"Skiing",
"url":"/store/outdoor-recreation/skiing",
"children":[
]
}
]
},
{
"name":"Physical Fitness",
"url":"/store/physical-fitness",
"children":[
]
},
{
"name":"Provident Living",
"url":"/store/provident-living",
"children":[
]
}
]
}
]
You mentioned you already have html renderer for trees, right? If you need further help let us know!
12 simple lines of code:
var rootList = $("<ul>").appendTo("body");
var elements = {};
$.each(menuItems, function() {
var parent = elements[this.url.substr(0, this.url.lastIndexOf("/"))];
var list = parent ? parent.next("ul") : rootList;
if (!list.length) {
list = $("<ul>").insertAfter(parent);
}
var item = $("<li>").appendTo(list);
$("<a>").attr("href", this.url).text(this.name).appendTo(item);
elements[this.url] = item;
});
http://jsfiddle.net/gilly3/CJKgp/
Although I like the script of gilly3 the script produces list with different element nesting of <li> and <ul> than was originally asked. So instead of
<li>Store
<ul>
<li>Travel</li>
...
</ul>
</li>
It produces
<li>Store
</li>
<ul>
<li>Travel</li>
...
</ul>
This may cause incompatibilities for utilities or frameworks working with such generated menu and producing interactive menu with animation (e.g. superfish.js).
So I updated the 12 lines script
var rootList = $("<ul>").appendTo("body");
var elements = {};
$.each(menuItems, function() {
var parent = elements[this.url.substr(0, this.url.lastIndexOf("/"))];
var list = parent ? parent.children("ul") : rootList;
if (!list.length) {
list = $("<ul>").appendTo(parent);
}
var item = $("<li>").appendTo(list);
$("<a>").attr("href", this.url).text(this.name).appendTo(item);
elements[this.url] = item;
});
http://jsfiddle.net/tomaton/NaU4E/
It's not in jQuery, but maybe this could help. I developed this after seeking the web to do exactly what you want.
http://www.chapleau.info/article/ArrayofUrlsToASitemap.html
Or maybe complete jQuery plugin http://jsfiddle.net/9FGRC/
(EDIT)
An update to previous version http://jsfiddle.net/9FGRC/1/
This version supports following case
var menuItems = [
{
name : "Store",
url : "/store"
},
{
name : "Cook Books",
url : "/store/healthy-eating/cook-books"
},
{
name : "Single Meal Gifts",
url : "/store/healthy-eating/single-meal-gifts"
}
]
Since there is skipped
{
name : "Healthy Eating",
url : "/store/healthy-eating"
},
It will produce following html
<ul>
<li>Store
<ul>
<li>Cook Books</li>
<li>Single Meal Gifts</li>
</ul>
</li>
</ul>
I guess it won't be the case, but could be helpful to someone
try something like this.
function Directory(parentNode) {
//Structure for directories. Subdirectories container as a generic object, initially empty
this.hasSubdirectories = false;
this.subdirectories = {};
//Render in steps. Until subdirectories or a link are added, all it needs is an LI and a blank anchor
this.nodeLi = document.createElement("li");
parentNode.appendChild(this.nodeLi);
this.nodeA = document.createElement("a");
this.nodeLi.appendChild(this.nodeA);
//if a subdirectory is added, this.nodeUl will be added at the same time.
}
Directory.prototype.setLabel = function (sLabel) {
this.nodeA.innerHTML = sLabel;
}
Directory.prototype.setLink = function (sLink) {
this.nodeA.href = sLink;
}
Directory.prototype.getSubdirectory = function (sPath) {
//if there were no previous subdirectories, the directory needs a new UL node.
if (!this.hasSubdirectories) {
this.nodeUl = document.createElement("ul");
this.nodeLi.appendChild(this.nodeUl);
this.hasSubdirectories = true;
}
//split the path string into the base directory and the rest of the path.
var r = /^\/?(?:((?:\w|\s|\d)+)\/)(.*)$/;
var path = r.exec(sPath);
//if the desired path is in a subdirectory, find or create it in the subdirectories container.
var subDirName = path[1] || path[2];
var subDir;
if (this.subdirectories[subDirName] === undefined) this.subdirectories[subDirName] = new Directory(this.nodeUl);
subDir = this.subdirectories[subDirName];
if (path[1] && path[2]) {
return subDir.getSubdirectory(path[2]);
} else {
return subDir;
}
}
function main(whichNode, aMenuItems) {
//whichNode is the node that is to be the parent of the directory listing.
//aMenuItems is the array of menu items.
var i;
var l = aItems.length;
var topDir = new Directory(whichNode);
//for each menu item, add a directory and set its properties.
var dirToAdd;
for (i = 0; i < l; i++) {
dirToAdd = topDir.getSubdirectory(aMenuItems[i].url);
dirToAdd.setLabel(aMenuItems[i].name);
dirToAdd.setLink(aMenuItems[i].url);
}
//and that's it.
}
how's that work?