Knockout custom binding doesn't update the computed function - javascript

I have a custom binding for an html editable field..
I changed it to use another custom binding now (HtmlValue), because EditableText had an error when updating the values (both custom bindings are included in the jsfiddle).
Anyone knows how to fix this?
This is the code that doesn't update the value:
ko.bindingHandlers.htmlValue = {
init: function (element, valueAccessor, allBindingsAccessor) {
ko.utils.registerEventHandler(element, "keyup", function () {
var modelValue = valueAccessor();
var elementValue = element.innerHTML;
if (ko.isWriteableObservable(modelValue)) {
modelValue(elementValue);
}
else { //handle non-observable one-way binding
var allBindings = allBindingsAccessor();
if (allBindings['_ko_property_writers'] && allBindings['_ko_property_writers'].htmlValue) allBindings['_ko_property_writers'].htmlValue(elementValue);
}
}
)
},
update: function (element, valueAccessor) {
var value = ko.utils.unwrapObservable(valueAccessor()) || "";
if (element.innerHTML !== value) {
element.innerHTML = value;
}
}
};
You can try it out here: http://jsfiddle.net/DMf8r/

There is a bunch of problems with the way the view model is constructed and with the bindings themselves...
The tax_total computed should be declared after lines because it accesses lines and Knockout executes tax_total as soon as the computed is created.
this needs to be passed into the computed so that this inside the computed is the view model
elem needs to be defined in the $.each() call
To loop the underlying array in $.each(), you need to use this.lines() instead of this.lines
The values inside lines need to be observables, otherwise the computed would not be notified of changes.
The span is using a value binding, it should be text.
There might have been more problems but it's hard to keep track of what all the changes were...
this.lines = ko.observableArray([
{ unit_price: ko.observable(5.0), tax_rate: ko.observable(21.00) },
{ unit_price: ko.observable(5.0), tax_rate: ko.observable(21.00) }]);
this.add_line = function () {
this.lines.push({ unit_price: ko.observable(5.0), tax_rate: ko.observable(21.00) });
}.bind(this);
this.tax_total = ko.computed(function () {
var total = 0; //this.subtotal()
$.each(this.lines(), function (index, elem) {
total += (elem.unit_price() * (elem.tax_rate() / 100));
});
return total;
}, this);
<span data-bind="text: tax_total">1.02</span>
Fiddle: http://jsfiddle.net/DMf8r/1/

Related

custom ko binding for jquery data tables

I have used jquery dataTable in knockout.js.In that i am generating data rows from function with remove link.It will remove row form table as well form observable array.The remove link works once and remove the row from table but when i try to remove another one they do not remove it.
Here you can check http://jsfiddle.net/zongweil/PLUKv/1/
$(document).ready(function () {
/* Custom binding */
ko.bindingHandlers.dataTable = {
init: function (element, valueAccessor) {
var binding = ko.utils.unwrapObservable(valueAccessor());
// If the binding is an object with an options field,
// initialise the dataTable with those options.
if (binding.options) {
$(element).dataTable(binding.options);
}
},
update: function (element, valueAccessor) {
var binding = ko.utils.unwrapObservable(valueAccessor());
// If the binding isn't an object, turn it into one.
if (!binding.data) {
binding = {
data: valueAccessor()
};
}
// Clear table
$(element).dataTable().fnClearTable();
// Rebuild table from data source specified in binding
$(element).dataTable().fnAddData(binding.data());
}
};
/* Object code */
function GroupMember(id, name, isGroupLeader) {
var self = this;
self.id = id;
self.name = name;
self.isGroupLeader = ko.observable(isGroupLeader);
self.link = ko.computed(function () {
return "/#user/" + self.id;
});
self.nameWithLink = ko.computed(function () {
return '' + self.name + '';
});
self.actions = ko.computed(function () {
return '<a class="btn btn-danger" data-bind="click: function() {removeMember(' + self.id + ')}">' + '<i class="icon-minus-sign"></i>' + '</a>';
});
}
/* View model */
var groupViewModel = {
groupMembers: ko.observableArray([
new GroupMember("1", "Abe", true),
new GroupMember("2", "Bob", false),
new GroupMember("3", "Bill", false)])
};
groupViewModel.membersTable = ko.computed(function () {
var self = this;
var final_array = new Array();
for (var i = 0; i < self.groupMembers().length; i++) {
var row_array = new Array();
row_array[0] = self.groupMembers()[i].nameWithLink();
row_array[1] = self.groupMembers()[i].actions();
final_array.push(row_array);
}
return final_array;
}, groupViewModel);
groupViewModel.removeMember = function (id) {
var self = this;
self.groupMembers.remove(function (groupMember) {
return groupMember.id == id;
});
};
ko.applyBindings(groupViewModel);
});
When you call fnClearTable in your custom binding's update function, you clear a part of your DOM behind knockout's back.
You then add new DOM elements by calling fnAddData.
Your buttons work using the click binding. For the click binding to work, knockout has to applyBindings.
If you want to keep using both the dataTable and a click binding to work with the DOM, you'll have to manually apply bindings every time you make a change. In your init method, let knockout know you're taking care of descendant bindings:
return { controlsDescendantBindings: true };
In your update method, apply bindings by hand:
ko.applyBindingsToDescendants(viewModel, element);
This makes sure your click bindings will work again.
Here's your example with this code added: http://jsfiddle.net/5t15rhyq/

knockout sortable with computed observable not working

jsfiddle example. Like the title says I am trying to use a computed observable along with rniemeyer knockout sortable example. I keep getting
the write method needs to be implemented
This error is viewable in the developer console.
I have a write method implement on my ko.computed but it still errors out. What I am I doing wrong?
html and javascript below
<div id="main">
<h3>Tasks</h3>
<div class="container" data-bind="sortable: tasks">
<div class="item">
<span data-bind="visible: !$root.isTaskSelected($data)">
</span>
<span data-bind="visibleAndSelect: $root.isTaskSelected($data)">
<input data-bind="value: name, event: { blur: $root.clearTask }" />
</span>
</div>
</div>
</div>
var Task = function(first,last) {
var self = this;
self.firstName = ko.observable(first);
self.lastName = ko.observable(last);
self.TestName = ko.computed({
read: function (){
return self.firstName() + " " + self.lastName();
},
write: function (item) {
console.log(item);
}
});
return self;
}
var ViewModel = function() {
var self = this;
self.testTasks = ko.observableArray([
new Task("test","one"),
new Task("test","two"),
new Task("test","three")
]);
self.tasks = ko.computed({
read: function() { return self.testTasks();},
write: function(item) {console.log(item);}
});
self.selectedTask = ko.observable();
self.clearTask = function(data, event) {
if (data === self.selectedTask()) {
self.selectedTask(null);
}
if (data.name() === "") {
self.tasks.remove(data);
}
};
self.addTask = function() {
var task = new Task("new");
self.selectedTask(task);
self.tasks.push(task);
};
self.isTaskSelected = function(task) {
return task === self.selectedTask();
};
};
//control visibility, give element focus, and select the contents (in order)
ko.bindingHandlers.visibleAndSelect = {
update: function(element, valueAccessor) {
ko.bindingHandlers.visible.update(element, valueAccessor);
if (valueAccessor()) {
setTimeout(function() {
$(element).find("input").focus().select();
}, 0); //new tasks are not in DOM yet
}
}
};
ko.applyBindings(new ViewModel());
As the very author of this plugin says here, you can't use a computed observable; the sortable plugin depends on an actual observable array.
Which makes sense when you think about it: the plugin is actually manipulating the various indexes of the array as you re-sort the elements.
Here's a "writableComputedArray" if you want the best of both worlds. If you add/remove from the array, and a subsequent re-compute of the observable performs the same add/remove, subscribers will not get notified the second time. However, it's your responsibility to make sure that there are no discrepancies between the computation of the array and what actually gets added/removed. You can accomplish this by making the necessary changes in the sortable binding's afterMove event.
ko.writeableComputedArray = function (evaluatorFunction) {
// We use this to get notified when the evaluator function recalculates the array.
var computed = ko.computed(evaluatorFunction);
// This is what gets returned to the caller and they can subscribe to
var observableArray = ko.observableArray(computed());
// When the computed changes, make the same changes to the observable array.
computed.subscribe(function (newArray) {
// Add any new values
newArray.forEach(function (value) {
var i = observableArray.indexOf(value);
if (i == -1) {
// It's a new value, push it
observableArray.unshift(value);
}
});
// Remove any old ones. Loop backwards since we're removing items from it.
for (var valueIndex = observableArray().length - 1; valueIndex >= 0; valueIndex--) {
var value = observableArray()[valueIndex];
var i = newArray.indexOf(value);
if (i == -1) {
// It's an old value, remove it
observableArray.remove(value);
}
}
});
return observableArray;
};

How to define a custom binding who use previous value to determine class in Knockout?

I need to bind a table with knockout, and I would like the table cell to get a different css class if the new value is higher or lower of the previous.
I have in mind different possibilities, such as storing the previous value in the bindingContext and have a function which returns the right class, but is it possible to add a custom binding handler which receives the previous value and the new value?
Although Jeff's and SÅ‚awomir's answers would work, I found an alternative that doesn't need any change to the view model nor relies on altering the DOM element object.
function subscribeToPreviousValue(observable, fn) {
observable.subscribe(fn, this, 'beforeChange');
}
ko.bindingHandlers['bindingWithPrevValue'] = {
init: function (element, valueAccessor) {
var observable = valueAccessor();
var current = observable();
console.log('initial value is', current);
subscribeToPreviousValue(observable, function (previous) {
console.log('value changed from', previous, 'to', current);
});
}
};
Naturally, that will only work if the bound property is an observable.
I looked into knockout source and I suppose that you can't access previous value inside update method of bindingHandler but you can store it inside element
ko.bindingHandlers['bindingWithPrevValue'] = {
update: function (element, valueAccessor) {
var prevValue = $(element).data('prevValue');
var currentValue = valueAccessor();
$(element).data('prevValue', currentValue());
// compare prevValue with currentValue and do what you want
}
};
What you could do is create an extender to extend the observables that you wish to track the previous values of. You could then inspect the previous value to do as you wish.
Just pass in the name of the property that will hold the previous value.
ko.extenders.previousValue = function (target, propertyName) {
var previousValue = ko.observable(null);
target[propertyName] = ko.computed(previousValue);
target.subscribe(function (oldValue) {
previousValue(oldValue);
}, target, 'beforeChange');
return target;
};
Then to use it:
function ViewModel() {
this.value = ko.observable('foo').extend({ previousValue: 'previousValue' });
}
var vm = new ViewModel();
console.log(vm.value()); // 'foo'
console.log(vm.value.previousValue()); // null
vm.value('bar');
console.log(vm.value()); // 'bar'
console.log(vm.value.previousValue()); // 'foo'
In your case, you could probably use something like this:
function TableCell(value) {
this.value = ko.observable(value).extend({ previousValue: 'previousValue' });
this.cssClass = ko.computed(function () {
// I'm assuming numbers
var current = Number(this.value()),
previous = Number(this.value.previousValue());
if (current < previous)
return 'lower';
else if (current > previous)
return 'higher';
}, this);
}

Knockout is slow when unchecking checkboxes on a large (1000) dataset

I am using this code, to check all checkboxes on my view.
var checked = self.includeAllInSoundscript();
var contents = self.filterContents(self.getFilters());
for (var i = 0; i < contents.length; i++) {
contents[i].includeInSoundscript(checked);
}
return true;
The checkbox
<input type="checkbox" data-bind="checked: includeInSoundscript" title="sometitle" />
This is what contents is:
(function (ko) {
ContentViewModel = function (data) {
this.orderId = data.orderId;
this.contentReferenceId = ko.observable(data.contentReferenceId);
this.includeInSoundscript = ko.observable();
});
This is the filter methods:
self.getFilters = function() {
var filterOrders = $.grep(self.orders(), function (order) {
return (order.usedInfilter());
});
var filterLocations = $.grep(self.locations(), function (location) {
return (location.usedInfilter());
});
return { orders: filterOrders, locations: filterLocations };
};
self.filterContents = function (filter) {
var filteredArray = self.contents();
if (filter.orders.length > 0) {
filteredArray = $.grep(filteredArray, function (content) {
return $.grep(filter.orders, function (order) {
return (order.orderId == content.orderId);
}).length > 0;
});
}
if (filter.locations.length > 0) {
filteredArray = $.grep(filteredArray, function (content) {
return $.grep(filter.locations, function (location) {
return $.inArray(location.location, content.orderedFrom().split('/')) != -1;
}).length > 0;
});
}
return filteredArray;
};
Checking all checkboxes is fast, but when i uncheck, it can take up to 20 seconds. Strange thing is when the filetered result is small, it still takes a bit longer, even if the filtered results is about 40, from a total set of 1000.
The checkbox is in a table, bound using data-bind="foreach: contents"
I have now removed some of the "unescessary" observables, for properties that most likely will not change, it then behaves slightly better, but still very slow, especially in firefox. The big question is, why is this behavior only on unchecking checkboxes, and not on filtering, sorting, checking, etc.
Notice: Its only unchecking the checkboxes, basically when "checked" is false, otherwise its fast.
Edit: I am only displaying 50 items at a time, but i am checking / unchecking all the filtered items. This, so that I have controll over what to post to the server.
This is what I use for this scenario. Maybe it will help you.
The checked binding can work with an array of selected items, but only supports storing strings in the array. I use a custom binding that supports storing objects in the array (like selectedOptions does):
ko.bindingHandlers.checkedInArray = {
init: function (element, valueAccessor) {
ko.utils.registerEventHandler(element, "click", function() {
var options = ko.utils.unwrapObservable(valueAccessor()),
array = options.array, // don't unwrap array because we want to update the observable array itself
value = ko.utils.unwrapObservable(options.value),
checked = element.checked;
ko.utils.addOrRemoveItem(array, value, checked);
});
},
update: function (element, valueAccessor) {
var options = ko.utils.unwrapObservable(valueAccessor()),
array = ko.utils.unwrapObservable(options.array),
value = ko.utils.unwrapObservable(options.value);
element.checked = ko.utils.arrayIndexOf(array, value) >= 0;
}
};
The binding for each checkbox then looks like this:
<input type="checkbox" data-bind="checkedInArray: { array: $parent.selectedItems, value: $data }" />
The checkbox for selecting all items uses the normal checked binding and is attached to a writable computed observable:
this.allItemsSelected = ko.computed({
read: function() {
return this.selectedItems().length === this.items().length;
},
write: function(value) {
this.selectedItems(value ? this.items.slice(0) : [] );
},
owner: this
});
Example: http://jsfiddle.net/mbest/L3LeD/
Update: Knockout 3.0.0 introduced the checkedValue binding option that makes the above custom binding unnecessary. You can now bind the checkboxes like this:
<input type="checkbox" data-bind="checked: $parent.selectedItems, checkedValue: $data" />
Example: http://jsfiddle.net/mbest/RLLX6/
What happens to performance if you use jQuery to check/uncheck all the boxes?
$('#tableId').find('input[type=checkbox]').prop('checked', checked);
Alternatively, could you check all the boxes when you display them, rather than doing all of them in one go?
Also, you could try using the knockout.utils methods for filtering the observable arrays, I'd be interested to see if there's any performance difference there.
var filteredArray = ko.utils.arrayFilter(this.items(), function(item) {
return ko.utils.stringStartsWith(item.name().toLowerCase(), filter);
});
There is also a method for looping over an array and processing each element:
ko.utils.arrayForEach(this.items(), function(item) {
var value = parseFloat(item.priceWithTax());
if (!isNaN(value)) {
total += value;
}
});
Again, I have no idea if this will help with performance or not, though I think it's a bit better prettiness-wise!

Invoke custom method on value changed

Is there a way to invoke one time some method on value changed? I created wrapper for bindingHandlers.value that invoke this method:
var update = bindingHandlers.value.update;
bindingHandlers.value.update = function(element, valueAccessor, allBindingAccessor, viewModel) {
var newValue = ko.utils.unwrapObservable(valueAccessor());
var elementValue = ko.selectExtensions.readValue(element);
var valueHasChanged = (newValue != elementValue);
update(element, valueAccessor, allBindingAccessor, viewModel);
if (valueHasChanged) {
myMethod();
}
}
Unfortunatelly when I change some value myMethod is called two times becuase come dependencyObservable is also changed. Any ideas?
If you just want to subscribe to a value changed, you can subscribe to any observable:
var viewModel = { property: ko.observable() };
viewModel.property.subscribe(function(newValue) {
//do stuff
});
To subscribe to all properties of an object you could do something like:
function subscribeAll(viewModel) {
for(var propertyName in viewModel) {
if(viewModel[propertyName].subscribe === 'function') {
viewModel[propertyName].subscribe(function(newValue) {
//do stuff
}
}
}
}

Categories

Resources