knockout.js nested foreach on nested array - mvvm

I've been looking at examples of nested foreach loops in knockout all afternoon, and I haven't been able to get anything working. My current setup, at least the relevant parts, are below.
ViewModel:
var sample = {
this.pageData = ko.observable();
this.panels = ko.observableArray();
};
ko.utils.extend(sample.prototype, {
activate: {
this.pageData(sampleData);
this.panels([
{
name: 'column1',
keys: ['key1', 'key2', 'key3'],
loadedWidgets: ko.observableArray()
},
{
name: 'column2',
keys: ['key4', 'key5'],
loadedWidgets: ko.observableArray()
},
{
name: 'column3',
keys: ['key6'],
loadedWidgets: ko.observableArray()
}
]);
this.loadWidgetPanels(this.panels(), this.pageData());
},
loadWidgetPanels: function (panels, pageData) {
for (var i = 0; i < panels.length; i++) {
var screens = filterContentByKey(pageData.Content, panels[i].keys);
if (screens) {
panels[i].loadedWidgets.push(widgetFactory.getWidgets(screens));
}
}
}
}
View:
<div>
<!-- ko foreach: panels -->
<div class="3columnStyle">
<!-- ko foreach: loadedWidgets -->
<!--ko compose: $data --><!-- /ko -->
<!-- /ko -->
</div>
<!-- /ko -->
</div>
I've confirmed that I'm getting back the right data in the right format in my loadedWidgets, but they don't display in the view. I can tell that the view at least knows how much data is there, because my DOM has a ko compose element for each widget. For example, the first column has a handful of widgets, and that div gets created with a handful of widgets in it. Column 2 has 2 widgets, and it gets 2 compose elements. Column 3 has 1 widget and gets one element. It's just not displaying the widgets themselves. Do I need additional elements or additional binding somewhere?
I have a working model of this that doesn't rely on nested loops. Instead of using the array of objects, it just creates each of the observable arrays. In other words, it's not looping. Instead of one array containing three objects, each with its own array, I have three arrays:
this.column1Widgets();
this.column2Widgets();
this.column3Widgets();
They're constructed the same way, just manually instead of looping. And the View looks more like this:
<div class="3columnStyle">
<!-- ko foreach: column3Widgets -->
<!-- ko compose: $data --><!-- /ko -->
<!-- /ko -->
</div>
<div class="3columnStyle">
<!-- ko foreach: column3Widgets -->
<!-- ko compose: $data --><!-- /ko -->
<!-- /ko -->
</div>
<div class="3columnStyle">
<!-- ko foreach: column3Widgets -->
<!-- ko compose: $data --><!-- /ko -->
<!-- /ko -->
</div>
I'm not sure why it's not working with the nested loop, but since the data is identical, I'm sure there's something I'm missing in setting up the View.

Without seeing the rest of your code, it's difficult to tell for sure. I'm a little suspicious of the sample object in your viewmodel. But it seems to me that you're not actually nesting your foreach's.
In your view, replace
foreach: loadedWidgets
with
foreach: $data.loadedWidgets
You need to reference the parent foreach in some way. $data represents the current item in the parent foreach, and that item, if I understand your model correctly, contains a loadedWidgets key.
Also, there's no need for containerless binding in your case.

As Eric Taylor suggested, it must have something to do with the containerless binding. I created a jsfiddle with some oversimplified object models, but changing my DOM from the above to the following immediately fixed the issue:
<div>Test</div>
<div data-bind="foreach: panels">
<ul data-bind="foreach: loadedWidgets">
<li data-bind="text: $data"></li>
</ul>
</div>
I don't think I have a good grasp of how the containers interact with the binding yet.

Related

Find out position of node in DOM tree?

I need to find out in which order two arbitrary DOM nodes appear.
Now before I start to write custom traversal code, I was wondering if there is any simple and cross browser way to find out a (comparable) location of a DOM node.
E.g.
<div> <!-- pos. 1 -->
<p> <!-- pos. 2 -->
<span>foo</span> <!-- pos. 3 -->
<span>bar</span> <!-- pos. 4 -->
</p>
<p> <!-- pos. 5 -->
<span>foo</span> <!-- pos. 6 -->
<span>bar</span> <!-- pos. 7 -->
</p>
</div>
Have you tried Node.compareDocumentPosition()? It returns a bitmask representing the calling node's relationship to the node passed as an argument. The reason it's a bitmask is because it may represent more than one of the possible relationships.
In this case, you can utilize Node.DOCUMENT_POSITION_FOLLOWING to determine the order in which two nodes appear (the result of this snippet will be in the console below the rendered page output):
const foo1 = document.getElementById("foo1");
const foo2 = document.getElementById("foo2");
if (foo1.compareDocumentPosition(foo2) & Node.DOCUMENT_POSITION_FOLLOWING) {
console.log("foo2 follows foo1");
} else {
console.log("foo2 precedes foo1");
}
<div>
<p>
<span id="foo1">foo</span>
<span>bar</span>
</p>
<p>
<span id="foo2">foo</span>
<span>bar</span>
</p>
</div>
It's widely supported, as reported by caniuse:

Using Protractor to select elements with by.repeater()

Using the following Protractor element and by.repeater() API methods below:
var targetRowText = 'Sales';
var targetGridName = 'myGrid';
var sel = 'grid-directive[grid-name="' + targetGridName + '"] .col-freeze .grid-wrapper';
var gridRows = element(by.css(sel).all(by.repeater('row in vm.sourceData.data'));
var result = gridRows.all(by.cssContainingText('span', targetRowText)).first();
I am able to select the following row element from a grid which I have labeled, myGrid:
<div id="rowId_21" ng-class-odd="'row-2'" ng-class-even="'row-3'" ng-class="vm.hideRow(row)" class="row-3 height-auto">
<div ng-repeat="column in vm.sourceData.columns" >
<div ng-if="!column.subCols" class="ng-scope">
<div ng-if="row[column.field].length !== 0" class="ng-scope highlight21">
<span ng-bind-html="row[column.field] | changeNegToPrenFormat" vm.highlightedrow="" class="ng-binding">
Sales
</span>
</div>
</div>
</div>
</div>
Please note that I have used by.cssContainingText() to look up the "Sales" span element.
MY PROBLEM:
That that I have located this row in var result, how can I retrieve the id attribute of that outer-most div ?
In other words, I need to selected <div id="rowId_21" so that I can reuse id="rowId_21" in a subsequent Protractor selector.
In jQuery, for example, I could use brute force to get that outer div id as follows :
var el = $('grid-directive[grid-name="Sales"] .col-freeze .grid-wrapper #rowId_21 span')
el.parentElement.parentElement.parentElement.parentElement;
Here's a high-level outlines of what I mean. The grid actually separates the left-most column from the actual data rows, so there are two distinct divs that accomplish this:
<div grid-directive grid-name="myGrid">
<div class="col-freeze" >
<!-- CONTAINS LEFT-MOST "CATEGORIES" COLUMN -->
</div>
<div class="min-width-grid-wrapper">
<!-- CONTAINS THE DATA ROWS-->
</div>
However, I'm struggling to do this in Protractor.
Advice is appreciated...
Bob
A straight-forward option would be to get to the desired parent element using the ancestor axis:
element(by.xpath("./ancestor::div[starts-with(#id, 'rowId')]")).getAttribute("id").then(function (parentId) {
// use parentId here
});
Though, I think that this going down and then up the tree should be considered as a sign that you are not approaching the problem in an easy and correct way.

KnockOutJS - Multiple ViewModels in a single View

I'm thinking that my application is getting quite large now, too large to handle each View with a single ViewModel.
So I'm wondering how difficult it would be to create multiple ViewModels and load them all into a single View. With a note that I also need to be able to pass X ViewModel data into Y ViewModel data so the individual ViewModels need to be able to communicate with each other or at least be aware of each other.
For instance I have a <select> drop down, that select drop down has a selected state which allows me to pass the ID of the selected item in the <select> to another Ajax call in a separate ViewModel....
Any points on dealing with numerous ViewModels in a single View appreciated :)
Knockout now supports multiple model binding. The ko.applyBindings() method takes an optional parameter - the element and its descendants to which the binding will be activated.
For example:
ko.applyBindings(myViewModel, document.getElementById('someElementId'))
This restricts the activation to the element with ID someElementId and its descendants.
See documentation for more details.
If they all need to be on the same page, one easy way to do this is to have a master view model containing an array (or property list) of the other view models.
masterVM = {
vmA : new VmA(),
vmB : new VmB(),
vmC : new VmC(),
}
Then your masterVM can have other properties if needed, for the page itself. Communication between the view models would not be difficult in this situation as you could relay through the masterVM, or you could use the $parent / $root in bindings, or some other custom options.
This is my answer after completing very large project with lots of ViewModels in single view.
Html View
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
</head>
<body>
<div id="container1">
<ul>
<li >Container1 item</li>
<!-- ko foreach: myItems -->
<li>Item <span data-bind="text: $data"></span></li>
<!-- /ko -->
</ul>
</div>
<div id="container2">
<ul>
<li >Container2 item</li>
<!-- ko foreach: myItems -->
<li>Item <span data-bind="text: $data"></span></li>
<!-- /ko -->
</ul>
</div>
<script src="js/jquery-1.11.1.js"></script>
<script src="js/knockout-3.0.0.js"></script>
<script src="js/DataFunction.js"></script>
<script src="js/Container1ViewModel.js"></script>
<script src="js/Container2ViewModel.js"></script>
</body>
</html>
For this view I'm creating 2 view models for id=container1 and id=container2 in two separate javascript files.
Container1ViewModel.js
function Container1ViewModel()
{
var self = this;
self.myItems = ko.observableArray();
self.myItems.push("ABC");
self.myItems.push("CDE");
}
Container2ViewModel.js
function Container2ViewModel() {
var self = this;
self.myItems = ko.observableArray();
self.myItems.push("XYZ");
self.myItems.push("PQR");
}
Then after these 2 viewmodels are registering as separate viewmodels in DataFunction.js
var container1VM;
var container2VM;
$(document).ready(function() {
if ($.isEmptyObject(container1VM)) {
container1VM = new Container1ViewModel();
ko.applyBindings(container1VM, document.getElementById("container1"));
}
if ($.isEmptyObject(container2VM)) {
container2VM = new Container2ViewModel();
ko.applyBindings(container2VM, document.getElementById("container2"));
}
});
Like this you can add any number of viewmodels for separate divs. But make sure do not create separate view model for a div inside registered div.
Check MultiModels plugin for Knockout JS - https://github.com/sergun/Knockout-MultiModels
We use components to achieve that. (http://knockoutjs.com/documentation/component-overview.html)
For example, we have this component library we are developing: https://github.com/EDMdesigner/knobjs
If you dig into the code, you will see that for example we reuse the knob-button component in several places.

jQuery next() selector when divs are not neighboring

Ive been using the following to change the width of the div.my-div that appears after the one you've clicked:
$(".my-div").click(function () {
$(this).next().css({'width':'500px'});
});
As my divs were neighboring, this worked fine:
<div class="my-div">stuff</div>
<div class="my-div">stuff</div>
<div class="my-div">stuff</div>
However now the structure has changed so they are no longer neighboring:
<div>
<div class="my-div">stuff</div>
</div>
<div>
<div>
<div class="my-div">stuff</div>
</div>
</div>
<div class="my-div">stuff</div>
Whats the simplest way to select the next element of the same class?
Thanks
jQuery will return elements in order of their appearance in the DOM.
As such, you could cache all the .my-div elements, use the index()[docs] method to get the index of the one that received the event, increment it and use the eq()[docs] method to get the next one.
var divs = $(".my-div"); // cache all of them
divs.click(function () {
var idx = divs.index( this ); // get the index in the set of the current one
divs.eq( idx + 1 ).css({'width':'500px'}); // get the one at the next index
});
This saves you from doing a bunch of unnecessary DOM selection and traversing.
Example: http://jsfiddle.net/VrATm/1/
EDIT: Posted wrong example link. Fixed.
You can traverse the tree hierarchy. That is, you can first jump to parent, then to next, then to children, like this:
$(this).parent().next().find(' > div').css({'width':'500px'});

Hieararchical data in MVC

In webforms I used a repeater inside a repeater(Hieararchical model). How can I achieve the same in MVC?
Example:
Data1 Data2
subdata1 subddata3
subdata2 subdata4
Data3 data4
subdata5 subdata7
subdata6 subdata8
I also require a two column layout as shown above. Any ideas ??
I can't remember where I read this, but it applies to you:
-- But won't we nead at least a repeater control in MVC?
-- We have a repeater control. It's called a for each loop
Let's say your view model has a property called Data of type IEnumerable<SuperDuper>. To iterate over it, you'd just do
<% foreach (var sd in Model.Data) { %>
<!-- write out fancy stuff -->
<% } <%>
To iterate over subdata, let's say that SuperDuper has a property named SubData that's also an IEnumerable<Something>. Nothing stops you from doing
<% foreach (var sd in Model.Data) { %>
<!-- write out some fancy stuff -->
<% foreach (var sub in sd.SubData) { %>
<!-- write out some more fancy stuff -->
<% }
} %>
For the two-column layout, resort to CSS.
And since Razor is on it's way, I can't resist to show you what those examples would look like with the new engine:
#foreach (var sd in Model.Data) {
<!-- write out fancy stuff -->
}
#foreach (var sd in Model.Data) {
<!-- write out some fancy stuff -->
#foreach (var sub in sd.SubData) {
<!-- write out some more fancy stuff -->
}
}
And with spark view engine you do this:
<div each="var item in Model.Data">
${item.Title}
<div each="var subItem in item.SubData" style="padding-left: 20px">
${subItem.Title}
<!-- Do some fancy stuff -->
</div>
</div>
Razor? Yuck! ;)