Knockout.js attr binding broken when use mapping - javascript

I'm trying to update ViewModel via mapping plugin with data sent by WebApi Controller on click. On first load everything is working fine, but when data updated, binding is broken.
I did tried to change attr binding to text binding and it's working nornally, so I suppose that problem is in attr binding. So here is JS:
<script>
function viewModel() {
var self = this;
self.currentPage = ko.observable();
self.pageSize = ko.observable(10);
self.currentPageIndex = ko.observable(0);
self.jobs = ko.observableArray();
self.currentPage = ko.computed(function () {
var pagesize = parseInt(self.pageSize(), 10),
startIndex = pagesize * self.currentPageIndex(),
endIndex = startIndex + pagesize;
return self.jobs.slice(startIndex, endIndex);
});
self.nextPage = function () {
if (((self.currentPageIndex() + 1) * self.pageSize()) < self.jobs().length) {
self.currentPageIndex(self.currentPageIndex() + 1);
}
else {
self.currentPageIndex(0);
}
}
self.previousPage = function () {
if (self.currentPageIndex() > 0) {
self.currentPageIndex(self.currentPageIndex() - 1);
}
else {
self.currentPageIndex((Math.ceil(self.jobs().length / self.pageSize())) - 1);
}
}
self.sortType = "ascending";
self.sortTable = function (viewModel, e) {
var orderProp = $(e.target).attr("data-column")
self.jobs.sort(function (left, right) {
leftVal = left[orderProp];
rightVal = right[orderProp];
if (self.sortType == "ascending") {
return leftVal < rightVal ? 1 : -1;
}
else {
return leftVal > rightVal ? 1 : -1;
}
});
self.sortType = (self.sortType == "ascending") ? "descending" : "ascending";
}
// Here is function that must update ViewModel
self.getDays = function (days) {
var uri = "/api/job?days=" + days;
$.getJSON(uri, function (data) {
ko.mapping.fromJS(data.$values, {}, self.jobs);
})
.error(function (xhr, status, error) {
var err = eval("(" + xhr.responseText + ")");
alert(err.Message);
});
}
$.getJSON("/api/job", function (data) {
self.jobs(data.$values);
});
}
$(document).ready(function () {
ko.applyBindings(new viewModel());
});
</script>
This is how I call update.
<div class="btn-group">
<button type="button" class="btn btn-default" id="7" value="7" data-bind="click: getDays.bind($data, '7')">7 дней</button>
<button type="button" class="btn btn-default" id="10" value="10" data-bind="click: getDays.bind($data, '10')">10 дней</button>
<button type="button" class="btn btn-default" id="14" value="14" data-bind="click: getDays.bind($data, '14')">14 дней</button>
<button type="button" class="btn btn-default" id="30" value="30" data-bind="click: getDays.bind($data, '30')">30 дней</button>
<button type="button" class="btn btn-default" id="60" value="60" data-bind="click: getDays.bind($data, '60')">60 дней</button>
<button type="button" class="btn btn-default" id="180" value="180" data-bind="click: getDays.bind($data, '180')">180 дней</button>
</div>
And this is the table that present data.
<table id="arrival" class="table table-condensed table-striped">
<thead>
<tr data-bind="click: sortTable">
<th></th>
<th data-column="excursion">Название</th>
<th data-column="excursiondate">Дата</th>
</tr>
</thead>
<tbody data-bind="foreach: currentPage">
<tr>
<td><a data-bind="attr: { href: '/list/' + $data.kodg }" target="_parent">Список туристов</a></td>
<%--<td data-bind="text: $data.kodg""></td>--%>
<td data-bind="text: $data.excursion"></td>
<td data-bind="text: $data.excursiondate"></td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3" class="pager">
<button data-bind="click: previousPage" class="btn previous"><i class="glyphicon glyphicon-backward"></i></button>
Страница
<label data-bind="text: currentPageIndex() + 1" class="badge"></label>
<button data-bind="click: nextPage" class="btn next"><i class="glyphicon glyphicon-forward"></i></button>
</td>
</tr>
</tfoot>
</table>
Any ideas?

Knockout mapping causes all properties to be converted into observables. So in your attr binding the kodg property is a function (ko.observable instance) and you should make a following change:
attr: { href: '/list/' + ko.utils.unwrapObservable($data.kodg) }
or simply
attr: { href: '/list/' + $data.kodg() }
if the kodg is always observable.

Try this
<a
data-bind="
attr: { href: $parent.Link($data.kodg) }"
target="_parent">
Список туристов
</a>
And in your viewmodel
self.Link = function(data){
return '/list/' + data
// Or if you have observable return '/list/' + data()
}

Related

Knockout binding for array is not recognized

I am having an issue with my knockout not being recognized in html:
<div class="row-fluid">
<div class="span6">
<div class="control-group">
<fieldset>
<legend>Rates</legend>
<div id="creditcards-rates" class="row-fluid">
<p data-bind="visible: rates() == undefined || rates().length == 0">
No rates added yet.
</p>
<table data-bind="visible: rates() !== undefined && rates().length > 0" class="table table-striped table-condensed">
<thead>
<tr>
<th>Payment Status</th>
<th>Income Amount</th>
<th>Confirmed by Provider</th>
<th>Payment Status</th>
<th>Notes</th>
<th></th>
</tr>
</thead>
<tbody data-bind="foreach: rates">
<tr>
<td>
<input type="hidden" data-bind="value: Id, attr: { id: 'Rates[' + $index() + '].Id', name: 'Rates[' + $index() + '].Id' }" />
</td>
<td>
<a id="btn-remove" data-bind="click: $root.removeRate"><i class="icon icon-minus"></i></a>
</td>
</tr>
</tbody>
</table>
<button class="btn" data-bind="click: addNewRate"><i class="icon icon-plus"></i> Add New Payment</button>
</div>
</fieldset>
</div>
</div>
</div>
Problem is where data-bind doesn't recognize the values. I get error in console:
Error: Syntax error, unrecognized expression: input[data-bind='value:
Id, attr: { id: 'Rates[' + $index() + '].Id', name: 'Rates[' +
$index() + '].Id' }']
...);return a},bc.error=function(a){throw new Error("Syntax error,
unrecognized exp...
My knockout is:
var Micki = Micki || {};
Micki.Electro = {
Edit: function (options) {
var settings = $.extend({
id: 0
}, options);
var _alerts;
var _rates;
var vm;
function init(alerts, rates) {
_alerts = alerts;
_rates = rates;
vm = new ViewModel();
ko.applyBindings(vm);
function ViewModel(options) {
var self = this;
self.mediator = null;
self.publishKey = options.publishKey;
self.rates = ko.observableArray([]);
self.init = function (mediator, alerts, rates) {
self.mediator = mediator;
self.alerts = alerts;
//Rates.
$.each(rates, function (index, rate) {
var _rate = {
Id: ko.observable(rate.Id),
PaymentType: ko.observable(rate.PaymentType),
IncomeAmount: ko.observable(rate.IncomeAmount),
ConfirmedDate: ko.observable(rate.ConfirmedDate),
PaymentStatus: ko.observable(rate.PaymentStatus),
Note: ko.observable(rate.Note)
};
self.rates.push(_rate);
});
self.container.on("change keypress paste focus textInput input", function () {
self.mediator.publish(self.publishKey);
});
};
self.initFields = function () {
self.fields = [];
_.each($("[name^='Rates']"), function (field) {
self.fields.push(new Crate.Core.Field($(field), self.mediator, self.publishKey));
});
//_ratesAndTiers.initFields();
//_campaignSpecific.initFields();
};
self.addNewRate = function (data, event) {
alert("test");
};
return self;
}
}
}
}

Live search on a web grid table with pagination working only on the actual page

I want to implement a live search on a web grid table which has pagination. However, my search only shows elements which are present in the actual page. I want my search function to do a search on all the element present in the table. Not only the ones actually displayed. Below is my search script:
<script type="text/javascript">
$(document).ready(function () {
$("#filter").keyup(function () {
// Retrieve the input field text and reset the count to zero
var filter = $(this).val(), count = 0;
console.log(filter);
// Loop through each row of the table
$("table tr").each(function () {
// If the list item does not contain the text phrase fade it out
if ($(this).text().search(new RegExp(filter, "i")) < 0) {
$(this).fadeOut();
// Show the list item if the phrase matches and increase the count by 1
} else {
$(this).show();
count++;
}
});
/* var numberItems = count;
$("#filter-count").text("Number of Comments = "+count);*/
});
});
my html page:
<div>
<div id="homeMessage">
Please see below the list of books available.
</div>
<br />
<div id="divCurrentRented">
<form id="live-search" action="" class="styled" method="post">
<fieldset>
<input type="text" class="form-control" id="filter" value="" placeholder="Search by title or author..."/>
<span id="filter-count"></span>
</fieldset>
</form>
</div>
<br />
<div id="divCurrentRented">
#{
WebGrid obj = new WebGrid(Model, rowsPerPage: 5);
}
#obj.Table(htmlAttributes: new
{
id="tableCurrentRented",
#class = "table"
},
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
rowStyle: "webgrid-row-style",
columns: obj.Columns(
obj.Column("Title", header: "Title"),
obj.Column("Author", header: "Author"),
obj.Column("Avaible", header: "Available", canSort:false),
obj.Column(header: "Rent", format:#<button type="button" class="btn btn-default">Rent it</button>)
))
</div>
<div style="text-align:right">
#obj.Pager(mode: WebGridPagerModes.All)
</div>
<br />
Any idea of how to do this?
The HTML:
<table id="tableCurrentRented" class="table">
<thead>
<tr class="webgrid-header">
<th scope="col">
Title </th>
<th scope="col">
Author </th>
<th scope="col">
Available </th>
<th scope="col">
Rent </th>
</tr>
</thead>
<tbody>
<tr class="webgrid-row-style">
<td>Le Bossu de Notre Dame</td>
<td>Victor Hugo</td>
<td>Yes</td>
<td><button type="button" class="btn btn-default" data-toggle="modal" data-target="#myModal" data-id="9" data-title="Le Bossu de Notre Dame">Yes</button></td>
</tr>
<tr class="webgrid-alternating-row">
<td>Oliver Twist</td>
<td>Charles Dickens</td>
<td>Yes</td>
<td><button type="button" class="btn btn-default" data-toggle="modal" data-target="#myModal" data-id="1" data-title="Oliver Twist">Yes</button></td>
</tr>
<tr class="webgrid-row-style">
<td>Pride and Prejudice</td>
<td>Jane Austen</td>
<td>Yes</td>
<td><button type="button" class="btn btn-default" data-toggle="modal" data-target="#myModal" data-id="5" data-title="Pride and Prejudice">Yes</button></td>
</tr>
<tr class="webgrid-alternating-row">
<td>Sense and Sensibility</td>
<td>Jane Austen</td>
<td>Yes</td>
<td><button type="button" class="btn btn-default" data-toggle="modal" data-target="#myModal" data-id="6" data-title="Sense and Sensibility">Yes</button></td>
</tr>
<tr class="webgrid-row-style">
<td>The Mayor of Casterbridge</td>
<td>Thomas Hardy</td>
<td>Yes</td>
<td><button type="button" class="btn btn-default" data-toggle="modal" data-target="#myModal" data-id="3" data-title="The Mayor of Casterbridge">Yes</button></td>
</tr>
</tbody>
</table>
My controller method:
public ActionResult SearchBooks()
{
var listBook = datamanager.GetAllBooks();
List<ViewBook> listViewBook = new List<ViewBook>();
foreach (Book b in listBook)
{
ViewBook viewBook = new ViewBook();
viewBook.BookID = b.BookId;
viewBook.Title = b.Title;
viewBook.Author = b.Author;
if (b.Rented ?? true)
{
viewBook.Avaible = "No";
}
else
{
viewBook.Avaible = "Yes";
}
listViewBook.Add(viewBook);
}
return View(listViewBook);
}
I have succeeded in passing all the models of my list to the javascript:
var data = #Html.Raw(Json.Encode(Model));
However, when I do this:
$(data).each(function () {
to loop through each element of data, I get this error:
Cannot use 'in' operator to search for 'opacity' in undefined
Any idea how I can solve this?
Javascript
$("#filter").keyup(function () {
$.ajax({
type: "GET",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: 'Search?q='+$("#filter").val(),
success:
function (result) {
$("#tableCurrentRented tbody").empty();
$.each(result.Books, function (index, item) {
var cls=(index%2==0)?"webgrid-row-style":"webgrid-alternating-row";
var html = ' <tr class="'+cls+'">'+
'<td>'+item.Title +'</td>'+
'<td>'+item.Author +'</td>'+
'<td>'+item.Avaible +'</td>'+
' <td><button type="button" class="btn btn-default" data-toggle="modal" data-target="#myModal" data-id="'+item.BookID +'" data-title="'+item.Title+'">'+item. +'</button></td></tr>';
$("#tableCurrentRented tbody").append(html);
});
},
error: function (xhr, status, err) {
}
});
});
Add new method for searching in controller
public ActionResult Search(string q)
{
var listBook = datamanager.GetAllBooks().Where(X=> X.Title.Contains(q)).ToList();
List<ViewBook> listViewBook = new List<ViewBook>();
foreach (Book b in listBook)
{
ViewBook viewBook = new ViewBook();
viewBook.BookID = b.BookId;
viewBook.Title = b.Title;
viewBook.Author = b.Author;
if (b.Rented ?? true)
{
viewBook.Avaible = "No";
}
else
{
viewBook.Avaible = "Yes";
}
listViewBook.Add(viewBook);
}
return Json(new { Books = listViewBook }, JsonRequestBehavior.AllowGet);
}
It seems you have a "Model" variable holding the data. Perhaps you should consider filtering the data in this Model directly and passing the filtered Model to the webgrid. The problem, how I understand it, is that you're trying to fetch already rendered values rather than considering the entire collection of data before rendering the filtered data.

Moment.js broken when Knockout.js model updated

I'm using WebApi 2.o together with Knockout.js and Moment.js to parse JSON date/time. Initially Moments.js are working fine when page loaded. But when I did updated Knockout ViewModel, Moment.js can't parse JSON date in proper way and show "Invalid date" error, regardless that WebApi return correct JSON date.
Here is java script:
<script>
function viewModel() {
var self = this;
self.currentPage = ko.observable();
self.pageSize = ko.observable(10);
self.currentPageIndex = ko.observable(0);
self.schedule = ko.observableArray();
self.currentPage = ko.computed(function () {
var pagesize = parseInt(self.pageSize(), 10),
startIndex = pagesize * self.currentPageIndex(),
endIndex = startIndex + pagesize;
return self.schedule.slice(startIndex, endIndex);
});
self.nextPage = function () {
if (((self.currentPageIndex() + 1) * self.pageSize()) < self.schedule().length) {
self.currentPageIndex(self.currentPageIndex() + 1);
}
else {
self.currentPageIndex(0);
}
}
self.previousPage = function () {
if (self.currentPageIndex() > 0) {
self.currentPageIndex(self.currentPageIndex() - 1);
}
else {
self.currentPageIndex((Math.ceil(self.schedule().length / self.pageSize())) - 1);
}
}
self.sortType = "ascending";
self.sortTable = function (viewModel, e) {
var orderProp = $(e.target).attr("data-column")
self.schedule.sort(function (left, right) {
leftVal = left[orderProp];
rightVal = right[orderProp];
if (self.sortType == "ascending") {
return leftVal < rightVal ? 1 : -1;
}
else {
return leftVal > rightVal ? 1 : -1;
}
});
self.sortType = (self.sortType == "ascending") ? "descending" : "ascending";
}
//This must update ViewModel - return data for selected number of days.
self.getDays = function (days) {
var uri = "/api/job?days=" + days;
$.getJSON(uri, function (data) {
ko.mapping.fromJS(data.$values, {}, self.schedule);
})
.error(function (xhr, status, error) {
var err = eval("(" + xhr.responseText + ")");
alert(err.Message);
});
}
}
$(document).ready(function () {
var vm = new viewModel();
$.ajax({
url: "/api/job",
type: "GET",
cache: false,
}).done(function (data) {
vm.schedule(data.$values);
ko.applyBindings(vm);
}).error(function (xhr, status, error) {
var err = eval("(" + xhr.responseText + ")");
alert(err.Message);
});
});
</script>
And this is HTML part:
<div class="well well-sm">
Find schedule for next
<div class="btn-group">
<button type="button" class="btn btn-default" id="7" value="7" data-bind="click: getDays.bind($data, '7')">7 days</button>
<button type="button" class="btn btn-default" id="10" value="10" data-bind="click: getDays.bind($data, '10')">10 days</button>
<button type="button" class="btn btn-default" id="14" value="14" data-bind="click: getDays.bind($data, '14')">14 days</button>
<button type="button" class="btn btn-default" id="30" value="30" data-bind="click: getDays.bind($data, '30')">30 days</button>
<button type="button" class="btn btn-default" id="60" value="60" data-bind="click: getDays.bind($data, '60')">60 days</button>
<button type="button" class="btn btn-default" id="180" value="180" data-bind="click: getDays.bind($data, '180')">180 days</button>
</div>
</div>
<table id="arrival" class="table table-condensed table-striped">
<thead>
<tr data-bind="click: sortTable">
<td></td>
<th data-column="excursion">Name</th>
<th data-column="excursiondate">Date</th>
</tr>
</thead>
<tbody data-bind="foreach: currentPage">
<tr>
<td><a data-bind="attr: { href: '/list/' + $data.kodg }" target="_parent">Tourist list</a></td>
<td data-bind="text: $data.excursion"></td>
<td data-bind="text: moment($data.excursiondate).format('DD.MM', 'ru')"></td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3" class="pager">
<button data-bind="click: previousPage" class="btn previous"><i class="glyphicon glyphicon-backward"></i></button>
Page
<label data-bind="text: currentPageIndex() + 1" class="badge"></label>
<button data-bind="click: nextPage" class="btn next"><i class="glyphicon glyphicon-forward"></i></button>
</td>
</tr>
</tfoot>
</table>
Here is default data I'm receiving on first call.
{"$id":"1","$values":[{"$id":"2","kodg":1406621521,"excursion":"Big Buddha Tour","excursiondate":"2014-07-31T00:00:00"},{"$id":"3","kodg":-1434407447,"excursion":"City Tour Gems","excursiondate":"2014-07-29T00:00:00"},{"$id":"4","kodg":-23317405,"excursion":"Transfer JSM","excursiondate":"2014-07-29T00:00:00"},{"$id":"5","kodg":-1035617799,"excursion":"Big Buddha Tour","excursiondate":"2014-07-26T00:00:00"},{"$id":"6","kodg":-277944785,"excursion":"City Tour Gems","excursiondate":"2014-07-25T00:00:00"},{"$id":"7","kodg":1405931911,"excursion":"City Tour Gems","excursiondate":"2014-07-22T00:00:00"},{"$id":"8","kodg":1405759887,"excursion":"City Tour Gems","excursiondate":"2014-07-20T00:00:00"},{"$id":"9","kodg":-699185234,"excursion":"Khao Lak","excursiondate":"2014-07-17T00:00:00"},{"$id":"10","kodg":2047068503,"excursion":"City Tour Gems","excursiondate":"2014-07-15T00:00:00"},{"$id":"11","kodg":164879331,"excursion":"City Tour Gems","excursiondate":"2014-07-13T00:00:00"},{"$id":"12","kodg":228070035,"excursion":"Shopping Tour","excursiondate":"2014-07-13T00:00:00"},{"$id":"13","kodg":1978323751,"excursion":"Khao Lak","excursiondate":"2014-07-10T00:00:00"}]}
And here is the set of data I'm receiving after I call getDays with parameter.
{"$id":"1","$values":[{"$id":"2","kodg":1406621521,"excursion":"Big Buddha Tour","excursiondate":"2014-07-31T00:00:00"}]}
I did change WebApi class to be as follow (in order to receive only strings):
public class job
{
public string kodg { get; set; }
public string excursion { get; set; }
public string excursiondate { get; set; }
}
But still has same issue. So it's not depend of data I'm receiving from WebApi controller.
It might help diagnose if you could put in a jsfiddle.
I created a simple view model:
function ViewModel() {
excursionDate = ko.observable('09/22/2014 23:49:35.349');
}
ko.applyBindings(new ViewModel());
and this html:
<span data-bind="text: moment(this.excursionDate()).format('DD.MM', 'ru')"></span>
It works with the above date, returning "22.09". I also works with just 09/22/2014, but with an invalid date like 09/22/2014 23:49.356, it does in fact return "invalid date" on the page. Maybe you could restrict your data to one line or something, scrub the data, debug it or even use a
<textarea data-bind="text: ko.toJSON($data)"></textarea>
to maybe see what's going on and to ensure you're not getting something you're not expecting. HTH

KnockoutJS Data disappears when I add a new item to array

View the issue on jsfiddle: http://jsfiddle.net/6bFsY/3/
When you click "Add Users" and then click "Add Users" again all of the data in the extensions drop down field disappears. This happened after I added an email column.
The email field gets pre-populated with whatever is selected in the extension dropdown (email is part of its object).
Also, the extensions drop down is unique per line, part of the script tells it to remove it from the array if it exists on a previous line.
JS
window.usrViewModel = new function () {
var self = this;
window.viewModel = self;
self.list = ko.observableArray();
self.pageSize = ko.observable(10);
self.pageIndex = ko.observable(0);
self.selectedItem = ko.observable();
self.extData = ko.observableArray();
self.validAccess = [{
'name': 'No Access',
'id': 'none'
}, {
'name': 'System Settings',
'id': 'pbx'
}, {
'name': 'Accounting',
'id': 'billing'
}, {
'name': 'Full Administrator',
'id': 'full'
}];
self.availableExtData = ko.computed(function () {
var inUse = [];
if (!self.selectedItem()) return inUse;
ko.utils.arrayForEach(self.list(), function (item) {
if (inUse.indexOf(item.usrExtVal().extension) == -1 && self.selectedItem() != item) inUse.push(item.usrExtVal().extension);
self.selectedItem().usrEmail(self.selectedItem().usrExtVal().email);
});
return ko.utils.arrayFilter(self.extData(), function (item) {
return inUse.indexOf(item.extension) == -1;
});
});
self.edit = function (item) {
if (self.selectedItem()) self.save();
self.selectedItem(item);
};
self.cancel = function () {
self.selectedItem(null);
};
self.add = function () {
if (self.selectedItem()) self.save();
var newItem = new Users();
self.selectedItem(newItem);
self.list.push(newItem);
self.moveToPage(self.maxPageIndex());
};
self.remove = function (item) {
if (confirm('Are you sure you wish to delete this item?')) {
self.list.remove(item);
if (self.pageIndex() > self.maxPageIndex()) {
self.moveToPage(self.maxPageIndex());
}
}
$('.error').hide();
};
self.save = function () {
self.selectedItem(null);
};
self.templateToUse = function (item) {
return self.selectedItem() === item ? 'editUsrs' : 'usrItems';
};
self.pagedList = ko.dependentObservable(function () {
var size = self.pageSize();
var start = self.pageIndex() * size;
return self.list.slice(start, start + size);
});
self.maxPageIndex = ko.dependentObservable(function () {
return Math.ceil(self.list().length / self.pageSize()) - 1;
});
self.previousPage = function () {
if (self.pageIndex() > 0) {
self.pageIndex(self.pageIndex() - 1);
}
};
self.nextPage = function () {
if (self.pageIndex() < self.maxPageIndex()) {
self.pageIndex(self.pageIndex() + 1);
}
};
self.allPages = ko.dependentObservable(function () {
var pages = [];
for (i = 0; i <= self.maxPageIndex(); i++) {
pages.push({
pageNumber: (i + 1)
});
}
return pages;
});
self.moveToPage = function (index) {
self.pageIndex(index);
};
};
ko.applyBindings(usrViewModel, document.getElementById('usrForm'));
function Users(fname, lname, email, phone, access, usrExtVal, usrEmail) {
this.fname = ko.observable(fname);
this.lname = ko.observable(lname);
this.email = ko.observable(email);
this.phone = ko.observable(phone);
this.access = ko.observable(access);
this.usrExtVal = ko.observable(usrExtVal);
this.usrEmail = ko.observable(usrEmail);
}
var ajaxResultExt = [{
'extension': '123',
'name': 'Stephen',
'email': 'test#test.com'
}, {
'extension': '123',
'name': 'Stephen',
'email': 'stephen#test.com'
}];
usrViewModel.extData(ajaxResultExt);
HTML
<fieldset title="Users">
<legend>2</legend>
<div>
<div class="cbp-content">
<form id="usrForm">
<h2>Users</h2>
<table class="table table-striped table-bordered" data-bind='visible: pagedList().length > 0'>
<thead>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Phone Number</th>
<th>Access</th>
<th>Extension</th>
<th>Email</th>
<th style="width: 100px; text-align:right;" />
</tr>
</thead>
<tbody data-bind=" template:{name:templateToUse, foreach: pagedList }"></tbody>
</table>
<!-- ko if: 2 > pagedList().length -->
<p class="pull-right"><a class="btn btn-primary" data-bind="click: $root.add" href="#" title="edit"><i class="icon-plus"></i> Add Users</a></p>
<!-- /ko -->
<div class="supOneUsr" style="display:none;"><i class="icon-warning-sign"></i> <span style="color:red;">Please supply at least 1 User with Administrator Rights</span></div>
<div class="pagination pull-left" data-bind='visible: pagedList().length > 0'>
<ul><li data-bind="css: { disabled: pageIndex() === 0 }">Previous</li></ul>
<ul data-bind="foreach: allPages">
<li data-bind="css: { active: $data.pageNumber === ($root.pageIndex() + 1) }"></li>
</ul>
<ul><li data-bind="css: { disabled: pageIndex() === maxPageIndex() }">Next</li></ul>
</div>
<br clear="all" />
<script id="usrItems" type="text/html">
<tr>
<td data-bind="text: fname"></td>
<td data-bind="text: lname"></td>
<td data-bind="text: phone"></td>
<td data-bind="text: access.asObject && access.asObject() && access.asObject().name"></td>
<td data-bind="text: usrExtVal().extension"></td>
<td data-bind="text: usrEmail"></td>
<td class="buttons">
<a class="btn" data-bind="click: $root.edit" href="#" title="edit"><i class="icon-edit"></i></a>
<a class="btn" data-bind="click: $root.remove" href="#" title="remove"><i class="icon-remove"></i></a>
</td>
</tr>
</script>
<script id="editUsrs" type="text/html">
<tr>
<td><input data-errorposition="b" class="required" name="fname" data-bind="value: fname" /></td>
<td><input data-errorposition="b" class="required" name="lname" data-bind="value: lname" /></td>
<td><input data-errorposition="b" class="required" name="phone" data-bind="value: phone" /></td>
<td><select class="accessSelect" data-bind="options: $root.validAccess, optionsText: 'name', optionsValue: 'id', value: access, valueAsObject: 'asObject'"></select></td>
<td><select id="extData" data-bind="options: $root.availableExtData, optionsText: 'extension', value: usrExtVal"></select></td>
<td><input id="extEmail" data-errorposition="b" class="required" name="email" data-bind="value: usrEmail" /></td>
<td class="buttons">
<a class="btn btn-success" data-bind="click: $root.save" href="#" title="save"><i class="icon-ok"></i></a>
<a class="btn" data-bind="click: $root.remove" href="#" title="remove"><i class="icon-remove"></i></a>
</td>
</tr>
</script>
</form>
</div>
</div>
</fieldset>
There are a few problems in your code.
availableExtData tries to access subproperties of usrExtVal, which is sometimes undefined. That access causes an error, which prevents further execution of the computed. So you need to first check if usrExtVal is set.
You have two entries in ajaxResultExt, but they both have the same extension. So once you've selected 123 for the first item, there aren't any left for the second, because both 123 values will be removed. So your extensions need to be unique.
You're updating usrEmail within a loop in availableExtData, which doesn't make any sense. It should be in a separate ko.computed.
Here is your example with these fixes: http://jsfiddle.net/mbest/6bFsY/5/

knockout js grid swap input fields if data exists

I have an editable grid in which I populate with data after a user logs in with ajax.
I'm populating it with a device list and shipping information. Inside the device list json I have a Boolean "byod", if the selected row has the device with this data set to "0" I'd like to swap the "MAC Address" text field with the "Ship To" drop down.
fiddle is here http://jsfiddle.net/QTUqD/15/, code is below:
<form id="extMngForm">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>Extension</th>
<th>Name</th>
<th>Email</th>
<th>Pin</th>
<th>Device</th>
<th>MAC Address</th>
<th>Ship To</th>
<th style="width: 100px; text-align:right;"></th>
</tr>
</thead>
<tbody data-bind=" template:{name:templateToUse, foreach: pagedList }"></tbody>
</table>
<p class="pull-right addExt"><a class="btn btn-primary" data-bind="click: $root.add" href="#" title="edit"><i class="icon-plus"></i> Add Extension</a></p>
<div class="pagination pull-left" data-bind='visible: pagedList().length > 0'>
<ul><li data-bind="css: { disabled: pageIndex() === 0 }">Previous</li></ul>
<ul data-bind="foreach: allPages">
<li data-bind="css: { active: $data.pageNumber === ($root.pageIndex() + 1) }"></li>
</ul>
<ul><li data-bind="css: { disabled: pageIndex() === maxPageIndex() }">Next</li></ul>
</div>
<br clear="all" />
<script id="extItems" type="text/html">
<tr>
<td style="width:20px;" data-bind="text: extension"></td>
<td data-bind="text: name"></td>
<td data-bind="text: email"></td>
<td style="width:20px;" data-bind="text: vmpin"></td>
<td data-bind="text: device.asObject && device.asObject() && device.asObject().name"></td>
<td data-bind="text: macAddress"></td>
<td data-bind="text: shipTo"></td>
<td class="buttons">
<a class="btn" data-bind="click: $root.edit" href="#" title="edit"><i class="icon-edit"></i></a>
<a class="btn" data-bind="click: $root.remove" href="#" title="remove"><i class="icon-remove"></i></a>
</td>
</tr>
</script>
<script id="editExts" type="text/html">
<tr>
<td style="width:20px;"><input style="width:65px;min-width: 65px;" data-errorposition="b" class="required" name="extension" data-bind="value: extension" /></td>
<td><input data-errorposition="b" class="required" name="name" data-bind="value: name" /></td>
<td><input data-errorposition="b" class="required" name="email" data-bind="value: email" /></td>
<td style="width:20px;"><input style="width:65px;min-width: 65px;" data-errorposition="b" class="required" name="vmpin" data-bind="value: vmpin" /></td>
<td>
<select data-bind="options: $root.devicesForItem($data), optionsText: 'name', optionsValue: 'id', value: device, valueAsObject: 'asObject'"></select>
</td>
<td><input name="macAddress" data-bind="value: macAddress" /></td>
<td><select style="width:100px;" data-bind="options: $root.addressList, optionsText: 'locationName', optionsValue: 'locationName', value: shipTo"></select></td>
<td class="buttons">
<a class="btn btn-success" data-bind="click: $root.save" href="#" title="save"><i class="icon-ok"></i></a>
<a class="btn" data-bind="click: $root.remove" href="#" title="remove"><i class="icon-remove"></i></a>
</td>
</tr>
</script>
</form>
window.ExtListViewModel = new function () {
var self = this;
window.viewModel = self;
self.list = ko.observableArray();
self.pageSize = ko.observable(10);
self.pageIndex = ko.observable(0);
self.selectedItem = ko.observable();
self.extQty = ko.observable(20);
self.devices = ko.observableArray([{"id":"gxp2100","name":"Grandstream GXP-2100","qty":"2","byod":"1"}, {"id":"gxp2100","name":"Grandstream GXP-2100 (BYOD)","qty":"1","byod":"0"}, {"id":"pcom331","name":"Polycom 331","qty":"2","byod":"0"}, {"id":"pcom331","name":"Polycom 331 (BYOD)","qty":"1","byod":"1"}]);
self.addressList = ko.observableArray(['addressList']);
self.availableDevices = ko.computed(function() {
var usedQuantities = {};
self.list().forEach(function(item) {
var device = item.device();
if (device) {
usedQuantities[device.id] = 1 + (usedQuantities[device.id] || 0);
}
});
return self.devices().filter(function(device) {
var usedQuantity = usedQuantities[device.id] || 0;
return device.qty > usedQuantity;
});
});
self.devicesForItem = function(item) {
var availableDevices = self.availableDevices();
return self.devices().filter(function(device) {
return device === item.device() || availableDevices.indexOf(device) !== -1;
});
}
self.edit = function (item) {
self.selectedItem(item);
};
self.cancel = function () {
self.selectedItem(null);
};
self.add = function () {
var newItem = new Extension();
self.list.push(newItem);
self.selectedItem(newItem);
self.moveToPage(self.maxPageIndex());
};
self.remove = function (item) {
if (confirm('Are you sure you wish to delete this item?')) {
self.list.remove(item);
if (self.pageIndex() > self.maxPageIndex()) {
self.moveToPage(self.maxPageIndex());
}
}
};
self.save = function () {
self.selectedItem(null);
};
self.templateToUse = function (item) {
return self.selectedItem() === item ? 'editExts' : 'extItems';
};
self.pagedList = ko.dependentObservable(function () {
var size = self.pageSize();
var start = self.pageIndex() * size;
return self.list.slice(start, start + size);
});
self.maxPageIndex = ko.dependentObservable(function () {
return Math.ceil(self.list().length / self.pageSize()) - 1;
});
self.previousPage = function () {
if (self.pageIndex() > 0) {
self.pageIndex(self.pageIndex() - 1);
}
};
self.nextPage = function () {
if (self.pageIndex() < self.maxPageIndex()) {
self.pageIndex(self.pageIndex() + 1);
}
};
self.allPages = ko.dependentObservable(function () {
var pages = [];
for (i = 0; i <= self.maxPageIndex() ; i++) {
pages.push({ pageNumber: (i + 1) });
}
return pages;
});
self.moveToPage = function (index) {
self.pageIndex(index);
};
};
ko.applyBindings(ExtListViewModel, document.getElementById('extMngForm'));
function Extension(extension, name, email, vmpin, device, macAddress, shipTo){
this.extension = ko.observable(extension);
this.name = ko.observable(name);
this.email = ko.observable(email);
this.vmpin = ko.observable(vmpin);
this.device = ko.observable(device);
this.macAddress = ko.observable(macAddress);
this.shipTo = ko.observable(shipTo);
};
ExtListViewModel.addressList = [{"shipping_address_street":"555 Lane","shipping_address_state":"TX","shipping_address_city":"Dallas","shipping_address_postalcode":"75000","locationName":"Preset"}, {"shipping_address_street":"555 Lane","shipping_address_state":"TX","shipping_address_city":"Dallas","shipping_address_postalcode":"75000","locationName":"Home"}];
//Shows device name not value (knockout extension)
ko.bindingHandlers.valueAsObject = {
init: function(element, valueAccessor, allBindingsAccessor) {
var value = allBindingsAccessor().value,
prop = valueAccessor() || 'asObject';
//add an "asObject" sub-observable to the observable bound against "value"
if (ko.isObservable(value) && !value[prop]) {
value[prop] = ko.observable();
}
},
//whenever the value or options are updated, populated the "asObject" observable
update: function(element, valueAccessor, allBindingsAccessor) {
var prop = valueAccessor(),
all = allBindingsAccessor(),
options = ko.utils.unwrapObservable(all.options),
value = all.value,
key = ko.utils.unwrapObservable(value),
keyProp = all.optionsValue,
//loop through the options, find a match based on the current "value"
match = ko.utils.arrayFirst(options, function(option) {
return option[keyProp] === key;
});
//set the "asObject" observable to our match
value[prop](match);
}
};
First of all, like I showed you in your other recent post, don't use optionsValue binding for your selects if you want the value to be the object itself (and then you don't need that valueAsObject stuff). So:
<select data-bind="options: $root.devicesForItem($data), optionsText: 'name', value: device"></select>
Second, your IDs need to be unique (not like in your current example) because your code depends on that.
Then, the rest is easy, you just need a simple computed boolean in your Extension objects that says whether the MAC address or the shipping thing is shown, e.g.:
this.showMac = ko.computed(function() {
if (self.device())
return self.device().byod !== '0';
return true;
});
And use this computed in the bindings, e.g. <!-- ko if: showMac --> or <td data-bind="text: showMac() ? macAddress : shipTo>"
Fiddle: http://jsfiddle.net/antishok/QTUqD/16/

Categories