Dynamically accessing properties of knockoutjs observable array - javascript

I am using the below code for handling sort functionality. It is working for me. But is there any way to make the code as common and so i can use it whenever needed.
<span class="sorting" data-bind="click: function(){ ui.items.sort(function(a,b){ return a.Username < b.Username ? -1 : 1; }); }">User Name</span>
<span class="sorting" data-bind="click: function(){ ui.items.sort(function(a,b){ return a.Firstname < b.Firstname ? -1 : 1; }); }">
First Name</span>
<span class="sorting" data-bind="click: function(){ ui.items.sort(function(a,b){ return a.Lastname < b.Lastname ? -1 : 1; }); }">
Last Name</span>
scripts
ui = new ListUI(config);
ko.applyBindings(ui);
var ListUI = function ListUIF(config) {
this.items = ko.observableArray([]);
}
var item = function itemF(data) {
var self = this;
self.Username = ko.observable(data.Username);
self.Firstname = ko.observable(data.Firstname);
self.Lastname = ko.observable(data.Lastname);
}
The code above is working fine, but i didn't want the sorting code to be repeated.
<span class="sorting" data-bind="click: function(){ ui.items.sort(function(a,b){ return a.Lastname < b.Lastname ? -1 : 1; }); }">
Last Name</span>
Instead i want something like this
<span class="sorting" data-bind="click: sortFunction">
Last Name</span>
var sortFunction = function sortFunctionF(a, b){
return a.Username < b.Username : -1 : 1; //How to make this common for other property also like Firstname, Lastname etc.
}

There's basically two options: Have three separate functions sortByUsername, sortByFirstname and sortByLastname, or you could do a custom binding that takes in additional information and sets up the sort.
The second one could look something like this:
<span class="sorting" data-bind="sortFunction: 'Username'">User Name</span>
<span class="sorting" data-bind="sortFunction: 'Firstname'">
First Name</span>
<span class="sorting" data-bind="sortFunction: 'Lastname'}">
Last Name</span>
And then the custom binding for sortFunction:
ko.bindingHandlers.sortFunction = {
update: function(element, valueAccessor, allBindingsAccessor, viewModel) {
var sortBy = ko.utils.unwrapObservable(valueAccessor());
var items = viewModel.items;
$(element).unbind('click.sort').bind('click.sort', function() {
items.sort(function(a, b) {
return a[sortBy]() < b[sortBy]() ? -1 : 1;
});
});
}
}
Fiddle
Another option as mentioned by Joeseph would be to pass the property name into the click function, which would then return a function. I don't think this is as nice an option as the custom binding, but Here's a fiddle that does that:
<span class="sorting" data-bind="click: getSortFunction('Username')">User Name</span>
<span class="sorting" data-bind="click: getSortFunction('Firstname')">
First Name</span>
<span class="sorting" data-bind="click: getSortFunction('Lastname')}">
Last Name</span>
And then your viewmodel would change to add the function:
var ListUI = function ListUIF(items) {
var self = this;
self.items = ko.observableArray(items);
self.getSortFunction = function(prop) {
return function() {
self.items.sort(function(a, b) {
return a[prop]() < b[prop]() ? -1 : 1;
});
};
};
return self;
}

Related

angularjs select all checkbox function

I'm fairly new to angular and have a select all checkbox that checks all the boxes through ng-model/ng-checked.
<th>
<input type="checkbox" id="selectAll" ng-model="selectAll"/>
</th>
<th>
${Case Number}
</th>
<tr ng-repeat="item in c.onbCase>
<td><input type="checkbox" name="checkbox" ng-click="checkboxFunc(item)"
ng-model="item.checked"
ng-checked="selectAll || item.checked"/>
</td>
<td>{{item.number}}</td>
</tr>
I also have a function called checkboxFunc that sets item.selected to true if checked and throws the case number into an array:
$scope.onbNum = [];
$scope.checkboxFunc = function(item){
if(item.selected == false) {
if($scope.onbNum.indexOf(item.number)==-1){
$scope.onbNum.push(
item.number
)
}
item.selected = true;
} else {
if($scope.onbNum.indexOf(item.number)!==-1){
var pos = $scope.onbNum.indexOf(item.number);
$scope.onbNum.splice(pos,1)
}
item.selected = false;
}
}
While the Select All checkbox checks all the boxes when clicked upon, how do I fix my function so that all the case numbers get thrown into the array?
Don't use ng-checked and ng-model together on the same element.
From the Docs:
Note that [the ng-checked] directive should not be used together with ngModel, as this can lead to unexpected behavior.
— AngularJS ng-checked Directive API Reference
If you are using item.checked just to ensure which check box is checked then you can do something like this :
$scope.onbCase = [
{ number:1, selected:false },
{ number:2, selected:false },
{ number:3, selected:false }
];
Here is your functions :
$scope.onbNum = [];
$scope.checkAll = function(){
$scope.onbNum = [];
for(var i=0; i<$scope.onbCase.length;i++){
$scope.onbCase[i].selected = $scope.selectAll;
}
if($scope.selectAll === true){
for(var i=0; i<$scope.onbCase.length;i++){
$scope.onbNum.push($scope.onbCase[i].number);
}
}
}
$scope.checkboxFunc = function(item){
if(item.selected) {
if($scope.onbNum.indexOf(item.number)===-1){
$scope.onbNum.push(
item.number
)
}
}
else {
var pos = $scope.onbNum.indexOf(item.number);
if(pos>-1){
$scope.onbNum.splice(pos,1);
}
$scope.selectAll = false;
}
}
Here is your HTML Page :
<th>
<input type="checkbox" id="selectAll" ng-model="selectAll"
ng-click="checkAll()"/>
</th>
<th> ${Case Number}
</th>
<tr ng-repeat="item in onbCase">
<td><input type="checkbox" name="checkbox"
ng-click="checkboxFunc(item)" ng-model="item.selected"
ng-checked="selectAll || item.selected"/>
</td>
<td>{{item.number}}</td>
</tr>
Hope this will work or give you a idea for what you want.
I'm not sure to exactly know your question , but I hope my answer help you:
add ng-click to your selectAll input like below:
<input type="checkbox" id="selectAll" ng-model="selectAll" ng-change="checkAll()" />
and also add below function to you controller:
$scope.checkAll = function () {
if ($scope.selectAll == true) {
angular.forEach($scope.c.onbCase, function(item) {
if ($scope.onbNum.indexOf(item.number) == -1) {
$scope.onbNum.push(
item.number
);
}
});
} else {
angular.forEach($scope.c.onbCase, function (item) {
if ($scope.onbNum.indexOf(item.number) !== -1) {
var pos = $scope.onbNum.indexOf(item.number);
$scope.onbNum.splice(pos, 1);
}
});
}
}

Filtering function: use KO struggling

Try to convert this code:
JS:
self.filter = function() {
var s = $('#searchField').val();
console.log(s.toLowerCase().replace(/\b[a-z]/g,"KC"));
s = s.toLowerCase().replace(/\b[a-z]/g, function(self) {
console.log(self.toUpperCase());
return self.toUpperCase();
});
$(".locationList > li").each(function() {
console.log(this);
$(this).text().search(s) > -1 ? $(this).show() : $(this).hide();
});
for(var i = 0; i < self.placeList().length; i++) {
console.log(self.map);
self.placeList()[i].marker.setMap(self.placeList()[i].marker.title.search(s) > -1 ? map : null);
}
};
};
HTML:
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<input id="searchField" data-bind="event: {keyup: filter}" type="text" placeholder='search by name or city' value="">
<hr>
<ul class="locationList" data-bind="foreach: placeList">
<li>
...
to something like this:
Javascript
self.filterText = ko.observable("");
self.filteredList = ko.computed(function(){
var filter = self.filterText().toLowerCase();
return // your filter function. make sure you return an array of what you want!
}, this);
You should use the visible-binding on the li-tag in your repeater. Then it is straight forward to hide items not meeting the criteria. Something like this:
<ul data-bind="foreach:placeList">
<li data-bind="text:$data, visible: filter"></li>
</ul>

Remove selected row in table from Knockout observable array

Alright i have working code that removes a selected row(s) via a checkbox being checked. However i am running into the issue of enforcing that only one of the radio buttons can be checked at any given moment. My first approach is to tie a click event to the each radio button and if it gets clicked, it loops through the observable array and marks all "false." Then it simply flips the flag to true for the item that fired the event. I know this isn't the best way but my lack luster knowledge of knockout is forcing me down this path..even though this method doesn't work atm. Can anyone shed light on what i am doing wrong or how to properly wire this up?
The html for the table
<table class="accountGroups information" id="tblAccountGroups">
<tr>
<td width="125px;" style="font-weight: bold;">StandardAccountNo</td>
<td width="125px;" style="font-weight: bold; text-align: center;">Primary</td>
<td style="font-weight: bold;">Effective Date</td>
<td style="font-weight: bold;">End Date</td>
<td style="font-weight: bold;">Remove</td>
</tr>
<!-- ko foreach: NewAccountGroupDetails-->
<tr id="Model.NewAccountGroupDetails[0].AccountGroupName" class="acctgrp-row">
<td>
<div>
<input style="width: 100%;" data-bind="value: StandardAccountNo, attr: {name: 'NewAccountGroupDetails[' + $index() + '].StandardAccountNo'}" />
</div>
</td>
<td>
<div style="text-align:center;">
<input style="width:100%;" type="radio" data-bind="value: IsPrimary, attr: {name: 'NewAccountGroupDetails[' + $index() + '].IsPrimary'}, click: $parent.markIsPrimary" />
</div>
</td>
<td>
<div>
<input style="width:125px;" class="datepicker" data-bind="value: EffectiveDate, attr: {name: 'NewAccountGroupDetails[' + $index() + '].EffectiveDate'}" readonly="readonly" />
</div>
</td>
<td>
<div>
<input style="width:125px;" class="datepicker" data-bind="value: EndDate, attr: {name: 'NewAccountGroupDetails[' + $index() + '].EndDate'}" readonly="readonly" />
</div>
</td>
<td>
<div style="text-align:center;">
<input type="checkbox" data-bind="checked: markedForDeletion, attr: {name: 'NewAccountGroupDetails[' + $index() + '].MarkedForDeletion'}" />
</div>
</td>
</tr>
<!-- /ko -->
</table>
The JS below powers the page
////VIEW MODEL FOR KNOCKOUT////
var Detail = function () {
this.StandardAccountNo = ko.observable('');
this.IsPrimary = ko.observable(false);
this.EffectiveDate = ko.observable(formattedDate(new Date()));
this.EndDate = ko.observable(formattedDate(new Date()));
this.markedForDeletion = ko.observable(false);
};
var ViewModel = function () {
var rawList = '#Html.Raw(new System.Web.Script.Serialization.JavaScriptSerializer().Serialize(Model.NewAccountGroupDetails))';
this.NewAccountGroupDetails = ko.observableArray(convertJSONToKoObservableObject($.parseJSON(rawList)));
this.NewAccountGroupDetails.push(new Detail());
this.deleteMarkedItems = function () {
this.NewAccountGroupDetails.remove(function (item) {
return item.markedForDeletion();
});
};
this.markIsPrimary = function () {
for (i = 0; this.NewAccountGroupDetails().length > 0; i++) {
this.NewAccountGroupDetails[i].IsPrimary(false);
}
return item.IsPrimary(true);
};
this.addNew = function () {
this.NewAccountGroupDetails.push(new Detail());
$('.datepicker').each(function (i, obj) {
$(obj).datepicker({ changeYear: true, changeMonth: true });
});
}
};
ko.applyBindings(new ViewModel());
function convertJSONToKoObservableObject(json) {
var ret = [];
$.each(json, function (i, obj) {
var newOBJ = {};
for (prop in obj) {
newOBJ[prop] = ko.observable(obj[prop]);
}
ret.push(newOBJ);
});
return ret;
}
Once i have the page working the way i want it to, i'll look into syntax improvements such as ko mapping library for the array.
In your view model, construct the remove button like this:
viewModel.remove = function (row) {
console.log(row);
viewModel.NewAccountGroupDetails.remove(row);
};
Now, the current context is passed as the first argument to any callback in knockout. Therefore, if you add a button with data-bind="click: $parent.remove", it will call the viewModel.remove function with the row context.
<tr ...>
...
<td>
<button data-bind="click: $parent.remove">Remove</button>
</td>
</tr>
I'd need some extra information, but let me show you an example, and give you a few advices:
First, the advices:
in order to convert your regular object in an object with observable properties an arrays you can use the Knockout Mapping plugin.
you can omit the step of parsing the JSON. You can simply assigng the JSON to a var, like this: var JSON=*your serialized JSON*; (Don't forget the semicolon at the end.
instead of including so many code in the data-bind, like this: NewAccountGroupDetails['+ $index() + '].EndDate, do this calculation on the viewmodel itself, an use a computed named, for example EndDateName
your viewmodel should include a selectedRow observable. When the user selects the row, put the row there, and you can use a computed observable that determines if a row is the selected row or not.
take into account that you can bind events that invoke functions in your code, and this events carry the data associated to the DOM object that originated the event. I.e. if the users clicks a row associated to a account group detail, you'll receive it in the event.
Example for 2:
// Instead of:
var viewModelJson = '[{"name": "Pepe"},{"name":"Juan"}]';
var viewModel = $.parseJSON(viewModelJson);
// Do this directly:
var people = [{"name": "Pepe"},{"name":"Juan"}];
As 4 and 5 are not clear at once, this is a simple sample of what you want to achieve.
<ul data-bind="foreach: people">
<li data-bind="text: name, click: $root.select,
css: {red: $data == $root.selectedPerson()}" >
</li>
</ul>
NOTE that the css class red is applied when the condition true. And the condition is that the value bound to the current row is the same as the value in the selectedPerson observable.
And this is the corresponding JavaScript (remember to reference knockout mapping!!)
var people = [{"name": "Pepe"},{"name":"Juan"}];
var PeopleModel = function(people) {
var self = this;
self.selectedPerson = ko.observable(); // This will hold the selected person
self.people = ko.mapping.fromJS(people); // Note ko.mapping!!
self.select = function(person) { // event receives the current data as 1st param
self.selectedPerson(person);
}
self.delete = function(person) {
// find de index of person and remove 1 item from that index
self.people.splice(self.people.indexOf(person),1);
}
return self;
};
var peopleModel = new PeopleModel(people);
ko.applyBindings(peopleModel);
You can run the jsfiddle here.
If you change the click binding to invoke $root.delete instead of $root.select, you'll see the person dissapear from the list when clicking it. Of course, you can add an extra element to do so.
NOTE: you can read the docs on click binding on knockout js site.
And a last advice: it's much better to use Web API, or a method returning a JsonResult to recover the data directly from the server, and keep the js on a separate file.
UPDATE
A little bit mode code.
You can add this HTML:
<input type="button" data-bind="click: removeSelected" value="removeSelected"/>
And this method in the view model:
self.removeSelected = function() {
if (self.selectedPerson()) {
self.delete(self.selectedPerson());
}
};
If you do so, when clicking the button, if there is a selected item, it will be removed from the list.
UPDATE: Another, more comple example
Here you have a more complete example, in this fiddle, that includes the code below:
CSS:
body {
font-family: Arial;
}
.container {
margin: 10px 0;
border: solid 1px #ABF;
}
.container > div {
padding: 4px;
border: solid 1px #ABF;
position: relative;
}
.selected {
border: solid 1px #00A;
color: #00A;
background-color: #BCF;
}
HTML:
<div data-bind="foreach: people" class="container">
<div data-bind="click: $root.select,
css: {selected: $data == $root.selectedPerson()}" >
<!-- ko text: name --><!-- /ko -->
<input type="button" value="Remove"
style="right:3px;top:2px; position:absolute;"
data-bind="click:$root.delete"/>
</div>
</div>
<div data-bind="visible: selectedPerson()" >
<input type="button" data-bind="click: removeSelected" value="Remove Selected"/>
<input type="button" data-bind="click: unSelect" value="Deselect"/>
</div>
<div data-bind="visible: selectedPerson()" class="container">
<div>
Selected: <!-- ko text: selectedPerson().name --><!-- /ko -->
</div>
</div>
JavaScript:
var people = [{"name": "Pepe"},{"name":"Juan"},{"name":"Luis"},{"name":"Adolfo"}];
var PeopleModel = function(people) {
var self = this;
self.selectedPerson = ko.observable(); // This will hold the selected person
self.people = ko.mapping.fromJS(people); // Note ko.mapping!!
self.select = function(person) { // The event receives the current data as parameter
self.selectedPerson(person);
};
self.delete = function(person) {
// find de index of person and remove (splice) it from the observable array
self.people.splice(self.people.indexOf(person),1);
self.selectedPerson(null);
}
self.removeSelected = function() {
if (self.selectedPerson()) {
self.delete(self.selectedPerson());
}
};
self.unSelect = function() {
self.selectedPerson(null);
}
return self;
};
var peopleModel = new PeopleModel(people);
ko.applyBindings(peopleModel);
Try to temporarily save the selected row when you select it
function AccountGroupViewModel() {
var viewModel = this;
viewModel.selectedRow = null;
// ...
viewModel.selectRow = function (data) {
// ...
viewModel.selectedRow = data;
}
viewModel.remove = function () {
// ...
if (viewModel.selectedRow != null) {
this.NewAccountGroupDetails.remove(viewModel.selectedRow);
}
}
}

Managing an Array within an Array using KnockoutKS

I have an array within an array, for example I have the following objects:
{ruleGroups: [{
rules: [{
dataField1:ko.observable()
,operator:ko.observable()
,dataField2:ko.observable()
,boolean:ko.observable()
,duration:ko.observable()
}]
}]
};
How can I edit the array within the array?
I was able to improve the issue but still have problems with adding row when adding group, the new group works but the old groups run dead:
A working example is found here (http://jsfiddle.net/abarbaneld/UaKQn/41/)
Javascript:
var dataFields = function() {
var fields = [];
fields.push("datafield1");
fields.push("datafield2");
return fields;
};
var operators = function() {
var operator = [];
operator.push("Plus");
operator.push("Minus");
operator.push("Times");
operator.push("Divided By");
return operator;
};
var booleanOperators = function() {
var operator = [];
operator.push("Equal");
operator.push("Not Equal");
operator.push("Greater Than");
operator.push("Less Than");
operator.push("Contains");
operator.push("Etc...");
return operator;
};
var ruleObj = function () {
return {
dataField1:ko.observable()
,operator:ko.observable()
,dataField2:ko.observable()
,boolean:ko.observable()
,duration:ko.observable()
}
};
var ruleGroup = function() {
return rg = {
rules: ko.observableArray([new ruleObj()]),
addRow: function() {
rg.rules.push(new ruleObj());
console.log('Click Add Row', rg.rules);
},
removeRow : function() {
if(rg.rules().length > 1){
rg.rules.remove(this);
}
}
}
};
var ViewModel = function() {
var self = this;
self.datafields = ko.observableArray(dataFields());
self.operators = ko.observableArray(operators());
self.booleanOperators = ko.observableArray(booleanOperators());
self.groupRules = ko.observableArray([new ruleGroup()]);
self.addGroup = function() {
self.groupRules.push(new ruleGroup());
};
self.removeGroup = function() {
if(self.groupRules().length > 1){
self.groupRules.remove(this);
}
};
self.save = function() {
console.log('Saving Object', ko.toJS(self.groupRules));
};
};
ko.applyBindings(new ViewModel());
HTML
<div data-bind="foreach: { data: groupRules, as: 'groupRule' }" style="padding:10px;">
<div>
<div data-bind="foreach: { data: rules, as: 'rule' }" style="padding:10px;">
<div>
<select data-bind="options: $root.datafields(), value: rule.dataField1, optionsCaption: 'Choose...'"></select>
<select data-bind="options: $root.operators(), value: rule.operator, optionsCaption: 'Choose...'"></select>
<select data-bind="options: $root.datafields(), value: rule.dataField2, optionsCaption: 'Choose...',visible: operator"></select>
<select data-bind="options: $root.booleanOperators(), value: rule.boolean, optionsCaption: 'Choose...'"></select>
<input data-bind="value: rule.duration" />
<span data-bind="click: groupRule.addRow">Add</span>
<span data-bind="click: groupRule.removeRow">Remove</span>
</div>
</div>
<span data-bind="click: $parent.addGroup">[Add Group] </span>
<span data-bind="click: $parent.removeGroup">[Remove Group]</span>
</div>
</div>
<div>
<span data-bind="click:save">[Save]</span>
</div>
I was able to fix the issue by rearranging the function of ruleGroup to:
var ruleGroup = function() {
var rg = {
rules: ko.observableArray([new ruleObj()]),
addRow: function() {
rg.rules.push(new ruleObj());
console.log('Click Add Row', rg);
},
removeRow : function() {
if(rg.rules().length > 1){
rg.rules.remove(this);
}
}
}
return rg;
};
Not exactly sure why this made a difference but I think its due to now a new var is being created and referenced.
Working JSFiddle is found here http://jsfiddle.net/abarbaneld/UaKQn/

knockoutjs deselect/select all checkboxes when one or more items deselected

This is similar to, but different from other questions around this topic.
I have a table with a list of records, each having a select checkbox.
In the table header I have a "Select All" checkbox.
When the user checks/unchecks "Select All" the records are selected/unselected. This works fine.
However, I need to deselect my "Select All" checkbox when one or more of the records are deselected.
My markup:
<table>
<thead>
<tr>
<th>Name</th>
<th><input type="checkbox" data-bind="checked: SelectAll" /></th>
</tr>
</thead>
<tbody data-bind="foreach: $data.People">
<tr>
<td data-bind="text: Name"></td>
<td class="center"><input type="checkbox" data-bind="checked: Selected" /></td>
</tr>
</tbody>
</table>
My script (edited):
function MasterViewModel() {
var self = this;
self.People = ko.observableArray();
self.SelectAll = ko.observable(false);
self.SelectAll.subscribe(function (newValue) {
ko.utils.arrayForEach(self.People(), function (person) {
person.Selected(newValue);
});
});
}
my.Person = function (name, selected) {
var self = this;
self.Name = name;
self.Selected = ko.observable(false);
}
This works
http://jsfiddle.net/AneL9/
self.SelectAll = ko.computed({
read: function() {
var item = ko.utils.arrayFirst(self.People(), function(item) {
return !item.Selected();
});
return item == null;
},
write: function(value) {
ko.utils.arrayForEach(self.People(), function (person) {
person.Selected(value);
});
}
});
but will give you a ordo n ^ 2 problem when selecting deselecting all, you can use a pasuable computed to get around that
http://www.knockmeout.net/2011/04/pausing-notifications-in-knockoutjs.html
edit: You can also extend the computed with a throttle, this way you avoid the ordo n^2 problem
.extend({ throttle: 1 })
http://jsfiddle.net/AneL9/44/
You should make SelectAll computed observable like this:
self.SelectAll = ko.computed({
read: function() {
var persons = self.People();
for (var i = 0, l = persons.length; i < l; i++)
if (!persons[i].Selected()) return false;
return true;
},
write: function(value) {
ko.utils.arrayForEach(self.People(), function(person){
person.Selected(value);
});
}
});
and strip SelectAll.subscribe out.
http://jsfiddle.net/Yqj59/

Categories