Knockout validation with the observable arrays - javascript

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>

Related

View data binding in nested dynamic observableArray

I need to put observableArray with dynamic size inside observableArray and bind data to View.
View is render values from newData, but not update it in newData, when i edit it in View.
ko.applyBindings(new (function () {
var self = this;
self.lengthNewData = ko.observable(2);
self.newData = ko.observableArray();
self.AddDataStrings = function () {
let newString = ko.observableArray();
for(let i0 = 0; i0 < self.lengthNewData(); i0++)
newString.push(i0);
self.newData.push(newString);
}
self.SendData = function () {
alert(ko.toJSON(self.newData));
}
})());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<span data-bind="visible: !newData().length">Number of rows: <input data-bind="value: lengthNewData"/></span>
<input type="button" value="Add" data-bind="click: $root.AddDataStrings"/>
<input type="button" value="Send" data-bind="visible: newData().length, click: $root.SendData"/><br>
<table>
<tbody data-bind="foreach: newData">
<tr data-bind="foreach: $data">
<td><input data-bind="value: $data"/></td>
</tr>
</tbody>
</table>
https://jsfiddle.net/tvxyyzkp/6/
Push Add, edit it and push Send. Data was not modified!
Why and What need to change?
There are two immediate mistakes in your code.
You do not push observables into your newString. You should do:
newString.push(ko.observable(i0));
You bind your inputs to $data. $data contains the unwrapped data value, i.e. it's not observable. Bind to $rawData in such a setup.
This works:
ko.applyBindings(new(function() {
var self = this;
self.lengthNewData = ko.observable(2);
self.newData = ko.observableArray();
self.AddDataStrings = function() {
let newString = ko.observableArray();
for (let i0 = 0; i0 < self.lengthNewData(); i0++)
newString.push(ko.observable(i0));
self.newData.push(newString);
}
self.SendData = function() {
alert(ko.toJSON(self.newData));
}
})());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<span data-bind="visible: !newData().length">Number of rows: <input data-bind="value: lengthNewData"/></span>
<input type="button" value="Add" data-bind="click: AddDataStrings" />
<input type="button" value="Send" data-bind="visible: newData().length, click: SendData" /><br>
<table>
<tbody data-bind="foreach: newData">
<tr data-bind="foreach: $data">
<td><input data-bind="value: $rawData" /></td>
</tr>
</tbody>
</table>
<pre data-bind="text: ko.toJSON($root, null, 2)"></pre>

Can't call controller function from view

I want to call the controller function from view.
This my view:
<body>
<div ng-app="appTable">
<div ng-controller="Allocation">
<button ng-click="add()"> Add </button>
<button ng-click="remove()">remove</button>
<table>
<th>
<td>Date</td>
<td>Project</td>
<td>Release</td>
<td>Feature</td>
<td>Module name</td>
<td>Hours spent</td>
<td>Comment</td>
</th>
<tr ng-repeat="data in dataList">
<td><input type="checkbox" ng-model="data.isDelete"/></td>
<td>
<input type="text"
datepicker
ng-model="data.date" />
</td>
<td><input type="text" ng-model="data.dept"/></td>
<td>
<select ng-model="data.release" ng-options="x for x in range">
</select>
</td>
<td>
<select ng-model="data.feature" ng-options="x for x in feature">
</select>
</td>
<td>
<input type = "text" ng-model = "data.modulename">
</td>
<td>
<select ng-model="data.hours" ng-options="x for x in hours">
</select>
</td>
<td>
<input type = "text" ng-model = "data.comment">
</td>
</tr>
</table>
<button ng-click="Submit()">Submit</button>
<table>
<tr ng-repeat="data in displayList">
<div ng-controller="Allocation as vm">
<div>{{vm.postdata(data.date)}}</div>
</div>
<p>Output Message : {{msg}}</p>
<p>StatusCode: {{statusval}}</p>
<p>Status: {{statustext}} </p>
<p>Response Headers: {{headers}} </p>
<td>
<p>{{data.date}}</p>
</td>
<td>
<p>{{data.dept}}</p>
</td>
<td>
<p>{{data.release}}</p>
</td>
<td>
<p>{{data.feature}} </p>
</td>
<td>
<p>{{data.modulename}}</p>
</td>
<td>
<p>{{data.hours}}</p>
</td>
<td>
<p>{{data.comment}}</p>
</td>
</td>
</tr>
</table>
</div>
</div>
</body>
This is script.
<script>
var app = angular.module("appTable", []);
app.controller("Allocation", function($scope) {
$scope.hours = ["1", "2", "3"];
$scope.range = ["1", "2", "3"];
$scope.feature = ["UT", "FSDS", "Coding/Devlopment", "QA"];
$scope.dataList = [{
date: '17/07/2016',
dept: 'OneCell',
release: '1',
feature: "UT",
modulename: "Redundancy",
hours: "1",
comment: "Add extra information"
}];
$scope.add = function() {
var data = {};
var size = $scope.dataList.length;
if (!size) {
$scope.dataList = [{
date: '17/07/2016',
dept: 'OneCell',
release: '1',
feature: "UT",
modulename: "Redundancy",
hours: "1",
comment: "Add extra information"
}];
} else {
size = size - 1;
data.date = $scope.dataList[size].date;
data.dept = $scope.dataList[size].dept;
data.release = $scope.dataList[size].release;
data.feature = $scope.dataList[size].feature;
data.modulename = $scope.dataList[size].modulename;
data.hours = $scope.dataList[size].hours;
data.comment = $scope.dataList[size].comment;
$scope.dataList.push(data);
}
};
$scope.Submit = function() {
$scope.test = "Submit is pressed...";
$scope.displayList = [];
angular.forEach($scope.dataList, function(v) {
if (!v.isDelete) {
$scope.displayList.push(v);
}
});
$scope.dataList.splice(0);
};
$scope.remove = function() {
var newDataList = [];
angular.forEach($scope.dataList, function(v) {
if (!v.isDelete) {
newDataList.push(v);
}
});
$scope.dataList = newDataList;
};
$scope.postdata = function(date) {
var data = {
date: date,
};
$http.post('/add_status/', data).then(function(response) {
if (response.data)
$scope.msg = "Post Data Submitted Successfully!";
}, function(response) {
$scope.msg = "Service not Exists";
$scope.statusval = response.status;
$scope.statustext = response.statusText;
$scope.headers = response.headers();
});
};
});
app.directive("datepicker", function() {
function link(scope, element, attrs, controller) {
// CALL THE "datepicker()" METHOD USING THE "element" OBJECT.
element.datepicker({
onSelect: function(dt) {
scope.$apply(function() {
// UPDATE THE VIEW VALUE WITH THE SELECTED DATE.
controller.$setViewValue(dt);
});
},
dateFormat: "dd/mm/yy" // SET THE FORMAT.
});
}
return {
require: 'ngModel',
link: link
};
});
</script>
As you can see in the view I have called the postdata function of the controller.This function internally uses msg variable.But view is not printing value of this variable.I am very new to Aj. Please help.
You need to make following changes:
Remove ng-controller="Allocation as vm", as you already defined controller above., and you not using this syntax in controller.
after you removed as vm, then no need to vm. calls.
you need to inject $http in your controller., to make API call
See this fiddle demo, I logged dummy text in console for each `submit click.
and to make single call on submit, See this Fiddle
You should return a value in the function or add a scope variable to be shown in the controller's div, because right now you are not showing anything.
<div ng-controller="Allocation as vm">
<div>{{vm.postdata(data.date)}}</div>
<p>Output Message : {{msg}}</p>
<p>StatusCode: {{statusval}}</p>
<p>Status: {{statustext}} </p>
<p>Response Headers: {{headers}} </p>
</div>
Anyway, you should not run procedural function in this way. Bind it to an event, ng-click or something.
And do not create other than td or th child elements in tr.
This is the proper html table structure:
<table>
<tr>
<th>Month</th>
<th>Savings</th>
</tr>
<tr>
<td>January</td>
<td>$100</td>
</tr>
</table>

Computed property based of external array Knockout JS

I just starting out with Knockout and running through a pluralsite course.
I have a (presumably basic) question about a computed property based on the parent array:
Given the below code how do I make it work so that the Amount property for each item in my.vm.attendees is always the totalCost divided by number of attendees. E.g. with 4 attendees in the collection amount for each should be 25.
This should be automatically updated as you add and remove items.
<script type="text/javascript">
$(function () {
var total = 100;
//Attendee construction
my.Attendee = function () {
this.Name = ko.observable();
this.Amount = ko.computed(function () {
return total / this.numberOfAttendees;
}, my.vm);
};
my.vm = {
//observable array of attendees
attendees: ko.observableArray([new my.Attendee()]),
addAttendee: function () {
my.vm.attendees.push(new my.Attendee());
},
numberOfAttendees: function () {
my.vm.attendees.lenght + 1; //zero based
}
}
ko.applyBindings(my.vm);
});
</script>
I would probably do something like this.
function attendee(name, amount) {
var self = this;
this.name = ko.observable(name);
this.amount = ko.observable(amount);
}
function model() {
var self = this;
this.attendees = ko.observableArray();
this.total = ko.computed(function() {
var total = 0;
ko.utils.arrayForEach(this.attendees(), function(item) {
var value = parseFloat(item.amount());
if (!isNaN(value)) {
total += value;
}
});
return total.toFixed(2);
}, this);
this.average = ko.pureComputed(function() {
return self.total() / self.attendees().length;
}, this);
this.remove = function(row) {
self.attendees.remove(row);
}
this.name = ko.observable('');
this.amount = ko.observable('');
this.add = function() {
self.attendees.push(new attendee(self.name(), self.amount()));
self.amount('');
self.name('');
}
}
var mymodel = new model();
$(document).ready(function() {
ko.applyBindings(mymodel);
mymodel.attendees.push(new attendee('Bob', 25));
});
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<table class="table table-condensed">
<thead>
<tr>
<th>Name</th>
<th>Amount</th>
<td>Delete</td>
</tr>
</thead>
<tbody data-bind="foreach: attendees">
<tr>
<td data-bind="text:name"></td>
<td data-bind="text:amount"></td>
<td>
<button class="btn btn-danger" data-bind="click: $parent.remove">
X
</button>
</td>
</tr>
</tbody>
<tfooter>
<tr>
<td colspan=2>Average:
<span data-bind="text:average">
</span>
</td>
</tr>
</tfooter>
</table>
<form class="form form-inline">
<label class="sr-only" for="inlineFormInput">Name</label>
<input type="text" class="form-control" id="inlineFormInput" placeholder="enter name" data-bind="textInput: name">
<label class="sr-only" for="inlineFormInputGroup">Amount</label>
<div class="input-group ">
<div class="input-group-addon">$</div>
<input type="text" class="form-control" id="inlineFormInputGroup" placeholder="amount" data-bind="textInput: amount">
</div>
<button type="buttont" class="btn btn-primary" data-bind="click: add">add row </button>
</form>
The "Amount" computed will be recalculated whenever any child observables are updated. Therefore you'll need to make "total" an observable if you want it to trigger a change, and you'll need to make "numberOfAttendees" a computed itself so that it can update when my.vm.attendees gets updated, and that can then cascade to an update of the "Amount" computed.

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