KnockoutJS Data disappears when I add a new item to array - javascript

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/

Related

Knockout JS: Creating dynamic table columns

Using Knockout JS:
I have a requirement as.
I have a table with 2 static columns where each has a text-box. I also have a add row button outside the table and one remove row button along each row.
When the user clicks add row it adds a row to the table and I can see two columns with textbox in each. User can add more rows if required by clicking add row and click remove row to remove rows.
This all has been setup and works fine as :
https://jsfiddle.net/aman1981/xmd1xobm/14/
My issue is there is also an Get Columns button. When the user clicks this button I fetch a list of columns and want to add these columns with headers b/w the already existing columns of the table.Each of these columns also need to have a textbox as well.
For ex I would the list of columns as:
var columnsList = 'name, address, zip';
I am not sure how to add columns dynamically. Would appreciate inputs.
Here is the setup of my table:
<table class="table table-bordered">
<thead class="mbhead">
<tr class="mbrow">
<th>SIID</th>
<th>Comment</th>
</tr>
</thead>
<tbody data-bind="foreach: data">
<tr>
<td>
<input type="text" data-bind="text: SIID" class="form-control textbox" />
</td>
<td>
<input type="text" data-bind="text: Comment" class="form-control textbox" />
</td>
<td>
<input type="button" value="Remove Row" data-bind="click: $parent.removeRow" class="btn btn-danger" />
</td>
</tr>
</tbody>
<div class="col-xs-12 col-sm-6">
<input type="button" value="Add Row" class="btn btn-primary" data-bind="click: addRow" />
<input type="button" value="Get Columns" class="btn btn-primary" data-bind="click: addColumns" />
</div>
Anyone?
Nice for you i have a dynamic table component from my previous project:
var observable = ko.observable;
var pureComputed = ko.pureComputed;
var observableArray = ko.observableArray;
var unwrap = ko.unwrap;
var data = observableArray([{
a: 123,
b: 234,
c: 345
},{
a: 1231,
b: 2341,
c: 3451
},{
a: 1232,
b: 2342,
c: 3425
},{
a: 1233,
b: 2343,
c: 3453
}]);
var columns = observableArray([{
field: "a",
displayName: "A"
},{
field: "b",
displayName: "B (i can even change the title)"
}])
function viewModel(params) {
var paramColumns = params.columns;
var paramData = params.data;
var paramFieldKey = params.fieldKey || "field";
var paramDisplayNameKey = params.displayNameKey || "displayName";
var koColumnHeaders = pureComputed(function () {
var columns = paramColumns();
var columnHeaders = [];
var fieldKey = unwrap(paramFieldKey);
var displayNameKey = unwrap(paramDisplayNameKey);
for (var i in columns) {
var column = columns[i];
columnHeaders.push({
field: column[fieldKey],
displayName: column[displayNameKey]
})
}
return columnHeaders;
})
var koRows = pureComputed(function () {
var data = paramData();
var columns = paramColumns();
var fieldKey = unwrap(paramFieldKey);
var rows = [];
for (var i in data) {
var datum = data[i];
var cells = []
var row = {
entity: data,
cells: cells
};
for (var j in columns) {
var column = columns[j];
cells.push(datum[column[fieldKey]] || "");
}
rows.push(row);
}
return rows;
});
return {
rows: koRows,
columns: koColumnHeaders,
}
}
ko.applyBindings(new viewModel({
data: data,
columns: columns
}))
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<table class="table table-bordered" border="1">
<thead>
<tr data-bind="foreach: columns">
<th data-bind="text: displayName">
</th>
</tr>
</thead>
<tbody data-bind="foreach: rows">
<tr>
<!-- ko foreach: cells -->
<td class="" data-bind="text: $data"></td>
<!-- /ko -->
</tr>
</tbody>
</table>
I was able to resolve this using the post at:
Knockout JS: Get Textbox data from Table under for-each binding
Here is my code:
(function() {
var ViewModel = function() {
var self = this;
self.valuesData = ko.observableArray();
self.columns = ko.computed(function() {
if (self.valuesData().length === 0)
return [];
return ValuesData.columns;
});
self.addRow = function() {
self.valuesData.push(new ValuesData());
};
self.Save = function() {
alert('Data:')
};
self.removeRow = function(data) {
self.valuesData.remove(data);
};
}
// Dynamic values.
var ValuesData = function(siid, comment) {
var self = this;
// Add observables dynamically for all relevant columns.
for (var i = 0; i < ValuesData.columns.length; i++) {
var column = ValuesData.columns[i];
self[column.Property] = ko.observable(column.InitialValue)
}
};
// Basic column definition.
ValuesData.columns = [{
Caption: 'SIID',
Property: 'SIID',
InitialValue: undefined
},
{
Caption: 'Column 1',
Property: 'Col1',
InitialValue: undefined
},
{
Caption: 'Column 2',
Property: 'Col2',
InitialValue: 'banana'
},
{
Caption: 'Comment',
Property: 'Comment',
InitialValue: undefined
}
]
vm = new ViewModel()
ko.applyBindings(vm);
// add initial row.
vm.addRow();
})();
Here is the html part:
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-
min.js"></script>
<br><br>
<table>
<thead>
<!-- NEW: use ko foreach with 'as' to have an alias for accessing $data. -->
<tr data-bind="foreach: { data: columns, as: 'column'}">
<th> <span data-bind="text: column.Caption"></span>
</th>
</tr>
</thead>
<tbody data-bind="foreach: { data: valuesData, as: 'rowData'}">
<tr data-bind="foreach: { data: $parent.columns, as: 'column' }">
<!-- NEW: bind to the corresponding property/observable in ValuesData -->
<td><input type="text" class="form-control textbox" data-bind="textInput:
rowData[column.Property]" /> </td>
</tr>
<tr>
<td>
<input type="button" value="Remove Row" data-bind="click:
$parent.removeRow" class="btn btn-danger" />
</td>
</tr>
</tbody>

Knockout validation with the observable arrays

I have a scenario where a user enters different users with details. I have text boxes for different fields for each user. I need to let user save the details if at least one field must be filled by user rather than all the fields for each row. No row should be completely empty. Here is a fiddle i am trying
on save i just need to ask user to enter only one value per row.Instead of asking for all the fields required.
http://jsfiddle.net/KHFn8/7161/
var NameModel = function(names) {
var self = this;
self.names = ko.observableArray(names);
self.addName = function() {
self.names.push(new xyz());
};
self.removeName = function(name) {
self.names.remove(name);
};
if (names != null)
self.names(names);
};
var xyz = function() {
var self = this;
self.FirstName = ko.observable().extend({
required: true
});
self.MiddleName = ko.observable().extend({
required: true
});
self.LastName = ko.observable().extend({
required: true
});
self.Gender = ko.observable().extend({
required: true
});
self.NameSuffix = ko.observable().extend({
required: true
});
};
ko.validation.init({
grouping: { deep: true, observable: true, live: true },
messagesOnModified: false
});
ViewModel = function() {
this.nameModel = new NameModel();
this.save = function() {
if (
this.errors().length == 0)
alert('Everything is valid, we can save!');
else if (
!this.nameModel.isValid())
alert('You must have at least one valid item ');
}
};
ko.applyBindings(new ViewModel());
<div data-bind="with: nameModel" class="panel-body">
<table class="table table-hover table-responsive">
<thead>
<tr>
<th>FirstName</th>
<th>MiddleName</th>
<th>LastName</th>
<th>Gender</th>
<th>NameSuffix</th>
</tr>
</thead>
<tbody data-bind="foreach: names">
<tr>
<td>
<input class="form-control" data-bind="value: FirstName" />
</td>
<td>
<input class="form-control" data-bind="value: MiddleName" />
</td>
<td>
<input class="form-control" data-bind="value: LastName" />
</td>
<td>
<input class="form-control" data-bind="value: Gender" />
</td>
<td>
<input class="form-control" data-bind="value: NameSuffix" />
</td>
<td style="vertical-align: inherit;">Delete</td>
</tr>
</tbody>
</table>
<button class="btn btn-success btn-sm" data-bind="click: addName.bind($data)">Add Name</button>
</div>
<button id="saveButton" type="button" class="btn btn-primary btn-company pull-left" data-bind="click: save">
Save
<span class="glyphicon glyphicon-download-alt"> </span>
</button>
There are some user-contributed KO-validation rules here:
https://github.com/Knockout-Contrib/Knockout-Validation/wiki/User-Contributed-Rules
This includes a rule for 'requires One Of', which will do what you want. However, I found the latest version on that page to be a bit buggy so I used a version from a project a did a while ago and applied it to your fiddle. The result is here:
http://jsfiddle.net/4f8s93k4/1/
The plugin is at the top of the JS and looks like:
ko.validation.rules['requiresOneOf'] = {
getValue: function (o) {
return (typeof o === 'function' ? o() : o);
},
validator: function (val, fields) {
var self = this;
var result = false;
ko.utils.arrayForEach(fields, function (field) {
var val = self.getValue(field);
if (val) {
result = true;
}
});
return result;
},
message: 'You must fill in at least one of these fields'
};
Rather than apply it to every one of your observables, I applied it using a single validation message, just to reduce code a bit:
self.requiresOneValidation = ko.observable().extend({
requiresOneOf: [self.FirstName, self.MiddleName, self.LastName, self.Gender, self.NameSuffix]
})
This then appears in your HTML like this:
<tr>
<td colspan="6">
<span data-bind="validationMessage: requiresOneValidation"></span>
</td>
</tr>

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;
}
}
}
}

Knockout.js attr binding broken when use mapping

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()
}

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