Nested menu using parent id in knockoutJs - javascript

I am try to create nested menu using given json data by the client.
Data :
var serverData = [
{
Id: "menuColorSearch",
Text: "Color search"
},
{
Id: "menuAncillaryProductMix",
Text: "Ancillary product mix"
},
{
Id: "menuDocuments",
Text: "Documents"
},
{
Id: "menuColorInfo",
ParentId: "menuDocuments",
Text: "Color info"
},
{
Id: "menuReports",
ParentId: "menuDocuments",
Text: "Reports"
},
{
Id: "menuMaintenance",
Text: "Maintenance"
},
{
Id: "menuPriceManagement",
ParentId: "menuMaintenance",
Text: "Price management"
}
];
I am trying like this :
var Menu = function(dept, all) {
var self = this;
this.id = dept.Id;
this.name = ko.observable(dept.Text);
this.parentId = dept.ParentId;
this.children = ko.observableArray();
ko.utils.arrayForEach(all || [], function(menu) {
if(menu.ParentId){
if (menu.ParentId === self.id) {
self.children.push(new Menu(menu, all));
}
}else{
new Menu(menu, all)
}
});
};
var ViewModel = function(data) {
this.root = new Menu(data[0], data);
};
$(function() {
ko.applyBindings(new ViewModel(serverData));
});
Templates :
<div data-bind="with: root">
<ul data-bind="template: 'deptTmpl'">
</ul>
</div>
<script id="deptTmpl" type="text/html">
<li>
<a data-bind="text: name"></a>
<ul data-bind="template: { name: 'deptTmpl', foreach: children }">
</ul>
</li>
</script>
problem is that its only work when 2nd and 3rd object has parent ID. i am trying something like it should make nested menu according to given json data. so id there is no parent id on object it should add on root. and if object has parent id it should add according to parent id.
Please help me to correct my code or guide me if these is another way to do this in KnockoutJS.
Thanks

This should help you http://jsfiddle.net/MCNK8/3/, the main idea is to rebuild main data array, by placing child inside parent
HTML
<script id="nodeTempl" type="text/html">
<li>
<a data-bind="text: Text"></a>
<ul data-bind="template: {name: nodeTemplate, foreach: children }"></ul>
</li>
</script>
<script id="nodeLeafTempl" type="text/html">
<li>
<a data-bind="text: Text"></a>
</li>
</script>
<ul data-bind="template: {name: nodeTemplate, foreach: children }"></ul>
Javascript (#see fiddle)
var serverData = [
{
Id: "menuColorSearch",
Text: "Color search"
},
{
Id: "menuAncillaryProductMix",
ParentId: 'menuColorSearch',
Text: "Ancillary product mix"
},
{
Id: "menuDocuments",
Text: "Documents"
},
{
Id: "menuColorInfo",
ParentId: "menuReports",
Text: "Color info"
},
{
Id: "menuReports",
ParentId: "menuDocuments",
Text: "Reports"
},
{
Id: "menuMaintenance",
ParentId: 'menuReports',
Text: "Maintenance"
},
{
Id: "menuPriceManagement",
ParentId: "menuMaintenance",
Text: "Price management"
}
];
function getNestedMenu(index, all) {
var root = all[index];
if(!root){
return all;
}
if(!all[index].children){
all[index].children = [];
}
for(var i = 0; i < all.length; i++){
//<infinity nesting?>
//put children inside it's parent
if(all[index].Id == all[i].ParentId){
all[index].children.push(all[i]);
all[i].used = true;
}
//this is needed for each item, to determine which template to use
all[index].nodeTemplate = function(node) {
return node.children.length > 0 ? 'nodeTempl' : 'nodeLeafTempl';
}
//</infinity nesting?>
}
return getNestedMenu(++index, all);
};
function getModel(data) {
var items = getNestedMenu(0, data);
//<remove duplicates, for infinity nesting only>
for(var i = 0; i < items.length; i++){
if(items[i].used){
items.splice(i, 1);
i--;
}
}
//</remove duplicates, for infinity nesting only>
//<build root item>
var model = {};
model.children = ko.observableArray(items);
model.nodeTemplate = function(node) {
return node.children.length > 0 ? 'nodeTempl' : 'nodeLeafTempl';
}
//</build root item>
console.log(items);
return model;
};
(function() {
//new ViewModel(serverData);
ko.applyBindings(getModel(serverData));
})();

Related

Knockout Table : Highlight a Table Row

I have an Example Fiddle here. In this Table I wish to achieve Highlighting a Particular Row selected. If unselected Row should not be highlighted.
One of many sample I found Fiddle but I am unable to incorporate them inside my Example Fiddle Above.
Below is the HTML Code which shows basic Table.
<table id="devtable">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Status</th>
</tr>
</thead>
<tbody data-bind="foreach: items">
<tr data-bind=" click: $parent.select ">
<td data-bind="text: ID"></td>
<td data-bind="text: Name"></td>
<td data-bind="text: Status"></td>
</tr>
</tbody>
ID :
Name :
Status :
Here is the knockout function to do manipulations
<Script>
var rowModel = function (id, name, status) {
this.ID = ko.observable(id);
this.Name = ko.observable(name);
this.Status = ko.observable(status);
};
var myData = [{
id: "001",
name: "Jhon",
status: "Single"
}, {
id: "002",
name: "Mike",
status: "Married"
}, {
id: "003",
name: "Marrie",
status: "Complicated"
}];
function MyVM(data) {
var self = this;
self.items = ko.observableArray(data.map(function (i) {
return new rowModel(i.id, i.name, i.status);
}));
self.select = function(item) {
self.selected(item);
self.enableEdit(true);
};
self.flashCss = ko.computed(function () {
//just an example
return 'flash';
});
self.selected = ko.observable(self.items()[0]);
self.enableEdit = ko.observable(false);
self.changeTableData = function() {
// How do I change the Data here and it should also reflect on the Page.
// If I do binding depending on condition it gives me error
if(true){
var myData = [{
id: "001",
name: "Jhon",
status: "Single"
}, {
id: "002",
name: "Mike",
status: "Married"
}, {
id: "003",
name: "Marrie",
status: "Complicated"
}];
}
else{
myData = [{
id: "111",
name: "ABC",
status: "Single"
}, {
id: "222",
name: "XYZ",
status: "Married"
}, {
id: "3333",
name: "PQR",
status: "Complicated"
}];
}
}
}
ko.applyBindings(new MyVM(myData));
</script>
CSS code below
.flash { background-color: yellow; }
You can use the css binding to add the .flash class based on the currently selected value:
<tr data-bind="click: $parent.select,
css: { flash: $parent.selected() === $data }">
...
</tr>
If you don't like this logic being defined in the view, you can pass a reference to the selected observable and create a computed property inside your RowModel:
var RowModel = function( /* ... */ selectedRow) {
// ...
this.isSelected = ko.pureComputed(function() {
return selectedRow() === this;
}, this);
}
Here's the quick fix in your fiddle:
http://jsfiddle.net/wa78zoe4/
P.S. if you want toggle-behavior, update select to:
self.select = function(item) {
if (item === self.selected()) {
self.selected(null);
self.enableEdit(false);
} else {
self.selected(item);
self.enableEdit(true);
}
};

Angular select all checkboxes from outside ng-repeat

Description
I have a small product order system, where a user can add order lines, and on each order line add one or more products. (I realise it's quite unusual for more than one product to be on the same order line, but that's another issue).
The products that can be selected on each line is based on a hierarchy of products. For example:
Example product display
T-Shirts
V-neck
Round-neck
String vest
JSON data
$scope.products = [
{
id: 1,
name: 'T Shirts',
children: [
{ id: 4, name: 'Round-neck', children: [] },
{ id: 5, name: 'V-neck', children: [] },
{ id: 6, name: 'String vest (exclude)', children: [] }
]
},
{
id: 2,
name: 'Jackets',
children: [
{ id: 7, name: 'Denim jacket', children: [] },
{ id: 8, name: 'Glitter jacket', children: [] }
]
},
{
id: 3,
name: 'Shoes',
children: [
{ id: 9, name: 'Oxfords', children: [] },
{ id: 10, name: 'Brogues', children: [] },
{ id: 11, name: 'Trainers (exclude)', children: []}
]
}
];
T-Shirts isn't selectable, but the 3 child products are.
What I'm trying to achieve
What I'd like to be able to do, is have a 'select all' button which automatically adds the three products to the order line.
A secondary requirement, is that when the 'select all' button is pressed, it excludes certain products based on the ID of the product. I've created an 'exclusion' array for this.
I've set up a Plunker to illustrate the shopping cart, and what I'm trying to do.
So far it can:
Add / remove order lines
Add / remove products
Add a 'check' for all products in a section, excluding any that are in the 'exclusions' array
The problem
However, although it adds the check in the input, it doesn't trigger the ng-change on the input:
<table class="striped table">
<thead>
<tr>
<td class="col-md-3"></td>
<td class="col-md-6"></td>
<td class="col-md-3"><a ng-click="addLine()" class="btn btn-success">+ Add order line</a></td>
</tr>
</thead>
<tbody>
<tr ng-repeat="line in orderHeader.lines">
<td class="col-md-3">
<ul>
<li ng-repeat="product in products" id="line_{{ line.no }}_product_{{ product.id }}">
{{ product.name }} <a ng-click="selectAll(product.id, line.no)" class="btn btn-primary">Select all</a>
<ul>
<li ng-repeat="child in product.children">
<input type="checkbox"
ng-change="sync(bool, child, line)"
ng-model="bool"
data-category="{{child.id}}"
id="check_{{ line.no }}_product_{{ child.id }}"
ng-checked="isChecked(child.id, line)">
{{ child.name }}
</li>
</ul>
</li>
</ul>
</td>
<td class="col-md-6">
<pre style="max-width: 400px">{{ line }}</pre>
</td>
<td class="col-md-3">
<a ng-click="removeLine(line)" class="btn btn-warning">Remove line</a>
</td>
</tr>
</tbody>
</table>
Javascript
$scope.selectAll = function(product_id, line){
target = document.getElementById('line_'+line+'_product_'+product_id);
checkboxes = target.getElementsByTagName('input');
for (var i = 0; i < checkboxes.length; i++) {
if (checkboxes[i].type == 'checkbox') {
category = checkboxes[i].dataset.category;
if($scope.excluded.indexOf(parseInt(category)) == -1)
{
checkboxes[i].checked = true;
// TODO: Check the checkbox, and set its bool parameter to TRUE
}
}
}
}
Update with full solution
There were a couple of issues with the above code. Firstly, I was trying to solve the problem by manipulating the DOM which is very much against what Angular tries to achieve.
So the solution was to add a 'checked' property on the products so that I can track if they are contained on the order line, and then the view is updated automatically.
One drawback of this method is that the payload would be significantly larger (unless it is filtered before being sent to the back-end API) as each order line now has data for ALL products, even if they aren't selected.
Also, one point that tripped me up was forgetting that Javascript passes references of objects / arrays, not a new copy.
The solution
Javascript
var myApp = angular.module('myApp', []);
myApp.controller('CartForm', ['$scope', function($scope) {
var inventory = [
{
id: 1,
name: 'T Shirts',
checked: false,
children: [
{ id: 4, name: 'Round-neck', checked: false, children: [] },
{ id: 5, name: 'V-neck', checked: false, children: [] },
{ id: 6, name: 'String vest (exclude)', checked: false, children: [] }
]
},
{
id: 2,
name: 'Jackets',
checked: false,
children: [
{ id: 7, name: 'Denim jacket', checked: false, children: [] },
{ id: 8, name: 'Glitter jacket', checked: false, children: [] }
]
},
{
id: 3,
name: 'Shoes',
checked: false,
children: [
{ id: 9, name: 'Oxfords', checked: false, children: [] },
{ id: 10, name: 'Brogues', checked: false, children: [] },
{ id: 11, name: 'Trainers (exclude)', checked: false, children: []}
]
}
];
$scope.debug_mode = false;
var products = angular.copy(inventory);
$scope.orderHeader = {
order_no: 1,
total: 0,
lines: [
{
no: 1,
products: products,
total: 0,
quantity: 0
}
]
};
$scope.excluded = [6, 11];
$scope.addLine = function() {
var products = angular.copy(inventory);
$scope.orderHeader.lines.push({
no: $scope.orderHeader.lines.length + 1,
products: products,
quantity: 1,
total: 0
});
$scope.loading = false;
}
$scope.removeLine = function(index) {
$scope.orderHeader.lines.splice(index, 1);
}
$scope.selectAll = function(product){
angular.forEach(product.children, function(item){
if($scope.excluded.indexOf(parseInt(item.id)) == -1) {
item.checked=true;
}
});
}
$scope.removeAll = function(product){
angular.forEach(product.children, function(item){
item.checked=false;
});
}
$scope.toggleDebugMode = function(){
$scope.debug_mode = ($scope.debug_mode ? false : true);
}
}]);
Click here to see the Plunker
You are really over complicating things first by not taking advantage of passing objects and arrays into your controller functions and also by using the DOM and not your data models to try to update states
Consider this simplification that adds a checked property to each product via ng-model
<!-- checkboxes -->
<li ng-repeat="child in product.children">
<input ng-model="child.checked" >
</li>
If it's not practical to add properties to the items themselves, you can always keep another array for the checked properties that would have matching indexes with the child arrays. Use $index in ng-repeat for that
And passing whole objects into selectAll()
<a ng-click="selectAll(product,line)">
Which allows in controller to do:
$scope.selectAll = function(product, line){
angular.forEach(product.children, function(item){
item.checked=true;
});
line.products=product.children;
}
With angular you need to always think of manipulating your data models first, and let angular manage the DOM
Strongly suggest reading : "Thinking in AngularJS" if I have a jQuery background?
DEMO
Why ng-change isn't fired when the checkbox is checked programatically?
It happens because
if($scope.excluded.indexOf(parseInt(category)) == -1)
{
checkboxes[i].checked = true;
// TODO: Check the checkbox, and set its bool parameter to TRUE
}
only affects the view (DOM). ng-change works alongside ngModel, which can't be aware that the checkbox really changed visually.
I suggest you to refer to the solution I provided at How can I get angular.js checkboxes with select/unselect all functionality and indeterminate values?, works with any model structure you have (some may call this the Angular way).

KnockoutJS Paged Grid example with a pageSize drop-down

Working from the KO example found here: http://knockoutjs.com/examples/grid.html,
I want to add a drop-down to select different page sizes (e.g. 4, 8, 12 items per page) and update the page grid upon changing the drop-down.
tried a bunch of things and I know I am missing something to get this to work. Thanks in advance for any help or a link to an existing solution.
What I sort of have now:
=== View ===
<div data-bind='simpleGrid: gridViewModel'> </div>
<select class="form-control" name="displayCount" id="displayCount" data-bind="value: valueDisplayCount;">
<option value="4">4</option><option value="8">8</option><option value="16">16</option>
</select>
<button data-bind='click: addItem'>
Add item
</button>
<button data-bind='click: sortByName'>
Sort by name
</button>
<button data-bind='click: jumpToFirstPage, enable: gridViewModel.currentPageIndex'>
Jump to first page
</button>
==== View Model =====
$( document ).ready(function(){
var initialData = [
{ name: "Well-Travelled Kitten", sales: 352, price: 75.95 },
{ name: "Speedy Coyote", sales: 89, price: 190.00 },
{ name: "Furious Lizard", sales: 152, price: 25.00 },
{ name: "Indifferent Monkey", sales: 1, price: 99.95 },
{ name: "Brooding Dragon", sales: 0, price: 6350 },
{ name: "Ingenious Tadpole", sales: 39450, price: 0.35 },
{ name: "Optimistic Snail", sales: 420, price: 1.50 }
];
var PagedGridModel = function(items) {
this.items = ko.observableArray(items);
this.valueDisplayCount = ko.observable(4);
this.sortByName = function() {
this.items.sort(function(a, b) {
return a.name < b.name ? -1 : 1;
});
};
this.jumpToFirstPage = function() {
this.gridViewModel.currentPageIndex(0);
};
this.valUpdDisplayCount= function(){
alert($('#displayCount').val());
this.gridViewModel.pageSize(6);
};
this.gridViewModel = new ko.simpleGrid.viewModel({
data: this.items,
columns: [
{ headerText: "Item Name", rowText: "name" },
{ headerText: "Sales Count", rowText: "sales" },
{ headerText: "Price", rowText: function (item) { return "$" + item.price.toFixed(2) } }
],
pageSize: this.valueDisplayCount
});
};
ko.applyBindings(new PagedGridModel(initialData));
});
JSFiddle:
http://jsfiddle.net/RNunc/1/
You would need to tweak the simplegrid code to look for an observable for pageSize. The updates could look like:
ko.simpleGrid = {
// Defines a view model class you can use to populate a grid
viewModel: function (configuration) {
this.data = configuration.data;
this.currentPageIndex = ko.observable(0);
this.pageSize = configuration.pageSize || ko.observable(5);
// If you don't specify columns configuration, we'll use scaffolding
this.columns = configuration.columns || getColumnsForScaffolding(ko.unwrap(this.data));
this.itemsOnCurrentPage = ko.computed(function () {
var size = ko.unwrap(this.pageSize);
var startIndex = size * this.currentPageIndex();
return ko.unwrap(this.data).slice(startIndex, startIndex + size);
}, this);
this.maxPageIndex = ko.computed(function () {
return Math.ceil(ko.unwrap(this.data).length / ko.unwrap(this.pageSize)) - 1;
}, this);
}
};
The simplegrid code is here: http://knockoutjs.com/examples/resources/knockout.simpleGrid.3.0.js
http://jsfiddle.net/rniemeyer/82MAR/

Why this knockout mapping code is not working?

I have a list of json objects which i want to map to knockout viewmodel list using ko mapping plugin. The below is my code (just to explain my problem, no need to take this code seriously):
var itemsList = [
{ Id: 1, Name: 'A' },
{ Id: 2, Name: 'B' },
{ Id: 3, Name: 'C' }
];
var item = function(data) {
ko.mapping.fromJS(data, {}, this);
}
var listOfItems = ko.observableArray();
var listOfItems = ko.mapping.fromJS(itemsList, {
create: function(options) {
return new item(options.data);
}
});
Now my listOfItems are always empty, why ??
You could really simplify this code and it is become working well.
var itemsList = [{ Id: 1, Name: 'A' }, { Id: 2, Name: 'B' }, { Id: 3, Name: 'C' }];
var viewModel = {
listOfItems: ko.mapping.fromJS(itemsList)
}
ko.applyBindings(viewModel);
Now you have viewModel with observableArray listOfItems, and all items in this array have Id and Name properties.
You can test it with folowing code:
<ul data-bind="foreach: listOfItems">
<li>
<span data-bind="text: Id"></span>
<span data-bind="text: Name"></span>
</li>
</ul>

items not within the <ul></ul>

I create a menu with a javascript object and jquery. I have certain items that needs to be in the <ul></ul> but instead they're beneath it.
http://jsfiddle.net/MWBt6/
I have 'Index' for example. In there I want to append a list of items.
I know the category id is 0 and the item id is 2, i stored those things in the data attribute.
Now how can I append a ul to that one?
That's not how append works. You need to create the elements on their own. Here:
var loadPath = "resources/books/book1/";
var menu = {
data: [{
name: "the book",
id: 0,
items: [{
name: "Introduction",
id: 0,
target: "inleiding.html"
}, {
name: "Content",
id: 1
}, {
name: "Index",
id: 2
}]
}, {
name: "my stuff",
id: 1,
items: [{
name: "Notes",
id: 0
}, {
name: "Marks",
id: 1
}]
}, {
name: "other",
id: 2,
items: [{
name: "Search",
id: 0
}, {
name: "Continue Reading",
id: 1
}]
}]
}
$(document).ready(function() {
var $menu = $('#menu');
for(var i = 0; i < menu.data.length; i++) {
var categorie = menu.data[i];
var categorieName = categorie.name;
var categorieId = categorie.id;
var items = categorie.items;
console.log("categorieName: " + categorieName);
var list = $('<ul>');
for(var j = 0; j < items.length; j++) {
var itemId = items[j].id;
list.append($('<li>').attr('data-itemId', itemId).text(items[j].name));
}
$menu.append(
$('<li>').attr('data-categorieId', categorieId).append(categorieName, list)
);
}
});​
Here's the updated jsFiddle.
You are trying to append as if the DOM is a text editor. You can't append the beginning of an element with it's opening tag, then later close that element with an append of a closing tag. Only full valid elements can be inserted into the DOM.
Instead, build an html string, then only make one append after the string is completed. This method is far more efficient than doing multiple appends also
Working demo: http://jsfiddle.net/MWBt6/3/

Categories