knockout.js recursive binding - binding

I'm trying to do some complex binding with knockout (at least for a newbie like me).
Consider the following data:
var originalData = {
id: 1,
name: "Main",
children: [ { id: 2, name: "bob", children: []}, { id: 3, name: "ted", children: [{id: 5, name:"albert"}, {id: 9, name: "fred"}]} ],
selectedChild: { id: 2, name: "bob" }
};
<table>
<tr>
<td data-bind="text: name"></td>
</tr>
<tr data-bind="if: children().length > 0">
<td>
<select data-bind="options: children,
optionsText: function(item){
return item.name;
},
optionsCaption: 'Choose...'"></select>
</td>
</tr>
Ok, that was the easy part.
The hard part, is that whenever an item is selected in the list, if this item has children then a new select box should appear underneath. Its datasource would be the children of the selected item in the first select box. Of course, it could continue on with any level of deepness.
How should I solve this problem with knockout ?
I've put together a sample of what I have so far on jsfiddle: http://jsfiddle.net/graphicsxp/qXZjM/

You can use recursive templates in knockout by putting the template into a script tag. Templates in a script tag can reference themselves, like this:
<div data-bind="template: 'personTemplate'"></div>
<script type="text/ko" id="personTemplate">
<span data-bind="text: name"></span>
<select data-bind="options: children, optionsText: 'name', optionsCaption: 'Choose', value: selectedChild"></select>
<!-- ko if: selectedChild -->
<div data-bind="template: { name: 'personTemplate', data: selectedChild }"></div>
<!-- /ko -->
</script>
Here is the fiddle
Update:
You can use a computed to easily do this, and remove the logic from the view (which I think is better in this case), and then bind the if to it.
self.showChildren = ko.computed(function() {
return self.selectedChild()
&& self.selectedChild().children().length > 0;
});
If you want to put both in the if block, you can, you just need to include the parens. The reason for this is that observables are functions; knockout lets you exclude them if you are just using the single reference, but they are required to "drill down" to their properties.
if: selectedChild() && selectedChild().children().length > 0
Here is the updated fiddle

Related

How to raise an event after knockout rendering is complete?

This jquery mobile table is being rendered with knockout.
<table data-role="table" id="report-table" class="ui-body-a ui-shadow ui-responsive table-stripe"
data-column-btn-theme="a" data-column-btn-text="Spalten..." data-column-popup-theme="a" data-mode="columntoggle"">
<thead>
<tr data-bind="foreach: columns">
<th data-bind="text: $data.Caption, attr: { 'data-priority': 1 + Math.floor($index() / 4) }"></th>
</tr>
</thead>
<tbody data-bind="foreach: { data: rows, afterRender: tableRowAfterRender }">
<tr data-bind="foreach: $parent.columns">
<!-- ko template: { name: $data.template } -->
<!-- /ko -->
</tr>
</tbody>
</table>
To get the "columntoggle" to actually work, I currently use the "aferRender" event:
self.tableRowAfterRender = function (element, data) {
// Skip unless data.Items()[i] is not the last element in the rows collections
for (var i = 0; i < data.Items().length - 1; i++) {
if (data.Items()[i] !== self.rows()[self.rows().length - 1].Items()[i])
return;
}
// refresh table after 100ms delay
setTimeout(function () { $("#report-table").table("refresh"); }, 100);
}
This is shaky, I hate the setTimeout() way of doing things, and this situation became quiet common for me with jquery mobile and knockout. I need a robust way to raise an event once all the knockout rendering or ideally once all the rendering concerned with elements inside the table-element is done. I was able to use custom bindings in some such situations, but I would not know how to do this here.
Try this. Wrap the table with: <div data-bind='template: { afterRender: myPostProcessingLogic }'>. Then do whatever you need to do in myPostProsssingLogic. This will only be called when the table is first rendered however. Here's a fiddle:
<div data-bind='template: { afterRender: myPostProcessingLogic }'>
<table data-role="table" id="report-table" class="ui-body-a ui-shadow ui-responsive table-stripe"
data-column-btn-theme="a" data-column-btn-text="Spalten..." data-column-popup-theme="a" data-mode="columntoggle"">
<thead>
<tr data-bind="foreach: columns">
<th data-bind="text: $data.Caption, attr: { 'data-priority': 1 + Math.floor($index() / 4) }"></th>
</tr>
</thead>
<tbody data-bind="foreach: { data: rows, afterRender: tableRowAfterRender }">
<tr data-bind="foreach: $parent.columns">
<!-- ko template: { name: $data.template } -->
<!-- /ko -->
</tr>
</tbody>
</table>
You can try to use binding init
ko.bindingHandlers.init = {
init: function(element, valueAccessor, allBindings, viewModel) {
var action = valueAccessor();
if (typeof action === 'function') {
setTimeout(action.bind(viewModel, element), 0);
}
}
};
Like this:
ko.components.register('simple-component', {
viewModel: function(params) {
this.title = params.title;
this.initHandler = function() {
console.log('DOM of component has been built.');
};
},
template:
'<div data-bind="init: initHandler">\
Title is: <span data-bind="text: title"></span>\
</div>'
});
Information for more complex cases is here - http://blog.atott.ru/2015/08/dom-built-event-for-knockout-components.html
There is a simple way of doing it. Here is the process
Assign a class to your row
<tr class="row" data-bind="foreach: $parent.columns">
Check in your function with if condition
if(data.Items().length == $('row').length){
$("#report-table").table("refresh")
}
Or call any event manually
$('#report-table').trigger('click')
Hope that's helpful to you.
EDITS:
Create a new observable and set it to false.
self.EnableTimeOut = ko.observable(false)
Now set it true according to your requirement. This is only example
if(data.Items().length == $('row').length && data.Items().length > 0){
self.EnableTimeOut(true)
$("#report-table").table("refresh")
}
And finally
Wrap it in condition
setTimeout(function () {
if(self.EnableTimeOut()){
$("#report-table").table("refresh");
}
}, 100);

add item to observable array in viewmodel from another partial view (MVC)

I am new to knockoutJS. I am working on an MVC application where I want to implement knockoutJS but the scenario is bit different.
I have a page where I am showing a list. I have 3 links on the page and on click of them I am adding partial views to page accordingly. What I want to do is that whenever I add values/data to partial views, the list which is on page should be updated with knockout. In other words I want to add value to observable array when I save data from partial view.
Please let me know if this is possible or I should keep it in jquery only.
Here is the code:
Main view:
<input type="button" value="Add Partial View" onclick="LoadPartial();" />
<div id="dvContent"></div>
<h4>People</h4>
<ul data-bind="foreach: people">
<li>
Name at position <span data-bind="text: $index"> </span>:
<span data-bind="text: name"> </span>
Remove
</li>
</ul>
<button data-bind="click: addPerson">Add</button>
<script src="~/Scripts/jquery-1.7.1.js"></script>
<script src="~/Scripts/knockout-2.1.0.js"></script>
<script>
function LoadPartial() {
$.ajax({
url: "/home/index",
dataType:"html",
type: "GET",
success: function (data) {
$("#dvContent").html(data);
}
});
}
</script>
<script>
function AppViewModel() {
var self = this;
self.people = ko.observableArray([
{ name: 'Bert' },
{ name: 'Charles' },
{ name: 'Denise' }
]);
self.addPerson = function () {
self.people.push({ name: "New at " + new Date() });
};
self.removePerson = function () {
self.people.remove(this);
}
}
ko.applyBindings(new AppViewModel());
</script>
Partial View:
<table>
<tr>
<td>Add new Row</td>
<td><input type="button" value="Add" data-bind="click: addPerson"/></td>
</tr>
</table>
Thanks,
JsHunjan
It is easy to accomplish with Knockout. You need to show some code that you have tried though if you want to get some help. I will post a general answer but it isn't going to fix your use case exactly, just basically -
Create an object to hold your new item, you can do this either in the parent or the child view model, but if you do it in the child you need to pass it back to the parent.
Once you hit a save button or add or whatever in the child view model just do a .push() into the observableArray that you created ex... - myObservableArray.push(newItem());
Knockout will recognize all of the changes taking place and perform the actions you want automatically.
Hope this helps.

checkbox data-bind using knockout.js and jquery mobile

I am facing problem to data-bind checkbox using knockout.js.
The jsfiddle http://jsfiddle.net/sajesh1985/ypbLN/
(HTML)Code:
<h2>View1</h2>
<h4>Select Columns:</h4>
<ul data-bind="foreach: gridOptions.columns" data-role="listview">
<div data-role="fieldcontain" >
<li id="li">
<label>
<input type="checkbox" id="chk" data-bind="event: {change: function(){ checked(!checked()); } }, checkbox: checked" /> <span data-bind="text: header"></span>
</label>
</li>
</div>
</ul>
<hr>
<table>
<thead>
<tr data-bind="foreach: gridOptions.columns">
<th data-bind="visible:checked, text: header"></th>
</tr>
</thead>
<tbody data-bind="foreach: people">
<tr data-bind="foreach: $parent.gridOptions.columns">
<td data-bind="text: $parent[dataMember], visible:checked"></td>
</tr>
</tbody>
</table>
JS Code:
var ProductSearchViewModel = {
gridOptions: {
columns: [{
header: 'First Name',
dataMember: 'firstName',
checked: ko.observable(true)
}, {
header: 'Last Name',
dataMember: 'lastName',
checked: ko.observable(true)
}]
},
people: [{
firstName: 'Bert',
lastName: 'Bertington'
}, {
firstName: 'Charles',
lastName: 'Charlesforth'
}, {
firstName: 'Denise',
lastName: 'Dentiste'
}]
};
ko.bindingHandlers.checkbox = {
update: function (element, valueAccessor) {
var value = valueAccessor();
var valueUnwrapped = ko.utils.unwrapObservable(value);
//$(element).checkboxradio().trigger('create');
$(element).attr("checked", valueUnwrapped).checkboxradio("refresh");
}
};
$(document).ready(function () {
ko.applyBindings(ProductSearchViewModel);
});
Can you please help me in rectifying the issue?
I am getting Uncaught cannot call methods on checkboxradio prior to initialization; attempted to call method 'refresh' in the browser.
The best way to bind checkboxes is to use the checked binding like here:
<input type="checkbox" id="chk" data-bind="checked: checked" />
See also documentation: http://knockoutjs.com/documentation/checked-binding.html

Binding issues with knockout and jQuery Mobile

UPDATE: The third bullet below ("so I tried the following" section) is the closest I have come to a fix. I think I basically need to 1) disable the button, 2) add ui-disable, 3) jqm refresh, all within the data-bind or model.
I am trying to get one of the knockout demos to run with jqm in order to build something similar in my project. It mostly works except that the submit button does not disable goes disabled but does not appear grayed out if items = 0.
If you remove jqm, the example works fine and the button turns gray. I realize that jqm can conflict with knockout due to dom manipulation so I tried the following:
Triggering style refresh in the model methods: $('button').button(); or $('.ui-page-active' ).page( 'destroy' ).page();
Starting the binding after pageinit. This broke the entire thing.
As a test, I tried a data-bind to set ui-disable instead of disabling the button. It applies the class but jqm needs a refresh somehow. Can I put code into the data-bind to do the refresh?
<button data-bind="css: {'ui-disable': gifts().length > 0}" type='submit'>Submit</button>
Here is the fiddle I have been using to troubleshoot this: http://jsfiddle.net/wtjones/wkEgn/
What am I missing?
<form action='/someServerSideHandler'>
<p>You have asked for <span data-bind='text: gifts().length'> </span> gift(s)</p>
<table data-bind='visible: gifts().length > 0'>
<thead>
<tr>
<th>Gift name</th>
<th>Price</th>
<th />
</tr>
</thead>
<tbody data-bind='foreach: gifts'>
<tr>
<td><input class='required' data-bind='value: name, uniqueName: true' /></td>
<td><input class='required number' data-bind='value: price, uniqueName: true' /></td>
<td><a href='#' data-bind='click: $root.removeGift'>Delete</a></td>
</tr>
</tbody>
</table>
<button data-bind='click: addGift'>Add Gift</button>
<button data-bind='enable: gifts().length > 0' type='submit'>Submit</button>
</form>
The model code:
var GiftModel = function(gifts) {
var self = this;
self.gifts = ko.observableArray(gifts);
self.addGift = function() {
self.gifts.push({
name: "",
price: ""
});
};
self.removeGift = function(gift) {
self.gifts.remove(gift);
};
self.save = function(form) {
alert("Could now transmit to server: " + ko.utils.stringifyJson(self.gifts));
// To actually transmit to server as a regular form post, write this: ko.utils.postJson($("form")[0], self.gifts);
};
};
var viewModel = new GiftModel([
{ name: "Tall Hat", price: "39.95"},
{ name: "Long Cloak", price: "120.00"}
]);
ko.applyBindings(viewModel);
// Activate jQuery Validation
//$("form").validate({ submitHandler: viewModel.save });
Yep. If you change button properties via JS (or using KO to change these props), then you must call the refresh method to update visual styling.
$('button').button('refresh');
So I suggest to create custom binding instead of default enable that updates mobile button styling (if applied):
ko.bindingHandlers.mobileEnable = {
update: function(el) {
ko.bindingHandlers.enable.update.apply(el, arguments);
$.fn.button && $(el).button('refresh');
}
}
and...
<button data-bind='mobileEnable: gifts().length > 0' type='submit'>Submit</button>
Corrected fiddle: http://jsfiddle.net/wkEgn/2/

one radio is not SELECTED?

I changed the approch, now, I get selected radio value correctly.
Issue: One of my radio is selected when initialize (first load then user can change the selection), but radio is not selected first time, how to initialize "Selected" appropriately?
#model Demo.Web.ViewModels.HomeViewModel
#{
ViewBag.Title = "Two Page";
}
#using (Html.BeginForm("Index", "Two", FormMethod.Post, new Dictionary<string, object> { { "data-bind", "submit:onSubmit" } }))
{
<table>
<tr>
<td>
<div data-bind="foreach: items">
<input type="radio" name="items" data-bind="attr: { value: id }, checked: $root.selected" />
<span data-bind="text: name"></span>
</div>
<div data-bind="text: selected">
</div>
</td>
</tr>
</table>
<input type="submit" name="send" value="Send" />
}
<script type="text/javascript">
var viewModel = {
items: [
{ "id": 1, "name": "one", "selected": false },
{ "id": 2, "name": "two", "selected": true },
{ "id": 3, "name": "three", "selected": false }
],
selected: ko.observable(),
onSubmit: function(){
var x = this.selected();
}
};
$(function() {
ko.applyBindings(viewModel);
});
When bound against inputs of type radio, the checked binding in Knockout tries to write the input's value to whatever value is bound against. The idea is that you all of the radios are bound against the same observable.
Something like:
var viewModel = {
items: [
{ id: 1, name: "one" },
{ id: 2, name: "two" },
{ id: 3, name: "three" }
],
selected: ko.observable()
};
Bound against a UI like:
<div data-bind="foreach: items">
<input type="radio" name="items" data-bind="attr: { value: id }, checked: $root.selected" />
<span data-bind="text: name"></span>
</div>
<div data-bind="text: selected"></div>
Sample: http://jsfiddle.net/rniemeyer/BbX4S/
So, in your case, you should be able to bind your radios against $root.IsSelected and use it rather than looping through the items.
When you initialize it, you might want to look to see if any button is checked, and then populate the root level IsSelected appropriately.

Resources