I'm trying to use a computed observable to create a custom div ID (e.g. branch3). However, anytime I attempt to bind the computed I get the "unable to parse bindings" error. I'm sure I could just go about doing this a different way, but I just don't understand why I can't use a computed here. I'm pretty sure I've seen it done.
Here is the jsfiddle I've been working on.
FIDDLE
var branchList =[{"Id":1,"Latitude":40.2444400000,"Longitude":-111.6608300000,"StreetAddress":"1525 W 820 N","BranchName":"GPS","City":"Cityplacwe","State":"UT","Zip":"84601"},{"Id":2,"Latitude":40.2455550000,"Longitude":-111.6616100000,"StreetAddress":"123 N Center","BranchName":"GPS Branch 2","City":"Lehi","State":"UT","Zip":"84043"}];
//myMarkers = new Array();
var Branch = function (data) {
var self = this;
self.Id = ko.observable(data.Id);
self.Latitude = ko.observable(data.Latitude);
self.Longitude = ko.observable(data.Id);
self.BranchName = ko.observable(data.BranchName);
self.StreetAddress = ko.observable(data.StreetAddress);
self.City = ko.observable(data.City);
self.State = ko.observable(data.State);
self.Zip = ko.observable(data.Zip);
this.DivId = ko.computed(function () {
return self.Id();
});
//self.DivId = ko.computed({
// //Reading from object to field
// read: function () {
// return "branch" + self.Id();
// },
// //writing from field to object
// write: function (value) {
// }
//});
}
var BranchViewModel = function () {
var self = this;
//create knockout array
self.branchArrayKO = ko.observableArray(branchList);
}
And the HTML
<div data-bind="foreach: branchArrayKO">
<div data-bind="attr: {'id': DivId}">
<p></p>
<h2></h2>
<ul>
<li data-bind="text: Id"></li>
<li data-bind="text: BranchName"></li>
<li data-bind="text: StreetAddress"></li>
</ul>
</div>
</div>
You need to convert your raw JavaScript array into an array of Branches. One way to do this is to use ko.utils.arrayMap to iterate over each item in the list and create a new Branch:
var BranchViewModel = function() {
var self = this;
//create knockout array
self.branchArrayKO = ko.observableArray(ko.utils.arrayMap(branchList, function(branch) {
return new Branch(branch);
}));
}
Updated example: http://jsfiddle.net/hawMW/2/
Another alternative that might be useful is the knockout mapping plugin, which you can use to automate all or part of the mapping process.
Related
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/
I have a working knockout js script, myValues is an array result of a ko.computed, then I pass it down to myValues2 which calls a myFunction to make each object in that array as "observable". This works but I wanted to call myFunction in self.myValues so that I wouldn't have to create another observableArray myValues2. Can you help me combine the script of myValues and myValues2 so that I can finally delete self.myValues2. Here is my code:
var myFunction = function (id, name, amount, automatic) {
var self = this;
self.Id = ko.observable(id);
self.Name = ko.observable(name);
self.Amount = ko.observable(amount);
self.Automatic = ko.observable(automatic);
};
self.myValues = ko.computed(function () {
return ko.utils.arrayFilter(self.completeList(), function (ded) {
return ded.automatic() == true;
});
});
self.myValues2 = ko.observableArray();
self.myValues2(ko.utils.arrayMap(self.myValues(), function (dd) {
return new myFunction(dd.id(), dd.name(), dd.amount(), dd.automatic());
}));
self.completeList()
how defined this array?
Because you began to filter this list by automatic field and then to fill other list. Strange order.
return ded.automatic() == true;
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;
};
I am building a sidebar to filter a main view, like for instance the one at John Lewis. I have the code working but it ain't pretty.
I know there are several SO questions on similar lines but I can't quite fathom my own use case.
I need to get the names of the checkboxes from the server ( eg via JSON ) to dynamically create observableArrays on my ShopView.
Here's how it is:
var data = {
'gender' : [ ],
'color' : [ ]
};
var filterMapping = {
create: function( obj ) {
return ko.observableArray( obj.data );
}
}
var ShopView = new function() {
var self = this;
ko.mapping.fromJS( { filters: data }, filterMapping, self );
// this is the bit I don't like
this.filterChange = ko.computed(function () {
for( var key in self.filters ) {
var obj = self.filters[key];
if( ko.isObservable(obj)){
obj();
}
}
});
this.filterChange.subscribe( function( ) {
//make AJAX request for products using filter state
});
}
My HTML looks as you'd expect:
Gender
<ul>
<li><input type="checkbox" value="male" data-bind="checked: filters.gender" />Male</li>
<li><input type="checkbox" value="female" data-bind="checked: filters.gender" />Female</li>
</ul>
As I say, it works, but it's not nice. In an ideal world I could subscribe to this.filters, eg
this.filters.subscribe( function() {
//make AJAX request for products using filter state
});
NB I'm not trying to do the filtering on the client side - just update my viewmodel when the dynamically-bound checkboxes change.
Any ideas? thanks!
First, the mapping plugin should be treated as an aid to code duplication. I don't think its a good idea to think of the mapping plugin as a solution in and of itself; at least not directly. It also obscures what is happening when you post your code on SO, since we can't see the models you are working with. Just a thought.
Now, ff you want to get dynamic filters from the server, and use them to filter a list of items (like you would in a store), I would do it something like this (here is the fiddle):
var FilterOption = function(name) {
this.name = name;
this.value = ko.observable(false);
};
var Filter = function(data) {
var self = this;
self.name = data.name;
options = ko.utils.arrayMap(data.options, function(o) {
return new FilterOption(o);
});
self.options = ko.observableArray(options);
self.filteredOptions = ko.computed(function() {
var options = []
ko.utils.arrayForEach(self.options(), function(o) {
if (o.value()) options.push(o.name);
});
//If no options, false represents no filtering for this type
return options.length ? options : false;
});
};
var ViewModel = function(data) {
var self = this;
self.items = ko.observableArray(data.items);
filters = ko.utils.arrayMap(data.filters, function(i) {
return new Filter(i);
});
self.filters = ko.observableArray(filters);
self.filteredItems = ko.computed(function() {
//Get the filters that are actually active
var filters = ko.utils.arrayFilter(self.filters(), function(o) {
return o.filteredOptions();
});
//Remove items that don't pass all active filter
return ko.utils.arrayFilter(self.items(), function(item) {
var result = true;
ko.utils.arrayForEach(filters, function(filter) {
var val = item[filter.name.toLowerCase()];
result = filter.filteredOptions().indexOf(val) > -1;
});
return result;
});
});
};
The next obvious step would be to add support for items that had multiple properties, but or options properties, but this should give you the basic idea. You have a list of filters, each with any number of options (which stack additively), and you use a computed items array to store the result of filtering the items.
Edit: To get the items using an ajax subscription, you would replace the FilteredItems prop with a computed that gets the selected filters, and then subscribe to it, like this:
var ViewModel = function(data) {
var self = this;
self.items = ko.observableArray(data.items);
filters = ko.utils.arrayMap(data.filters, function(i) {
return new Filter(i);
});
self.filters = ko.observableArray(filters);
self.selectedFilters = ko.computed(function() {
ko.utils.arrayFilter(self.filters(), function(o) {
return o.filteredOptions();
});
});
self.selectedFilters.subscribe(function() {
//Ajax request that updates self.items()
});
};
This is the same question as this:
https://stackoverflow.com/questions/2890620/jquery-running-a-function-in-a-context-and-adding-to-a-variable
But that question was poorly posed.
Trying again, I'm trying to do something like this:
I have a bunch of elements I want to grab data from, the data is in the form of classes of the element children.
<div id='container'>
<span class='a'></span>
<span class='b'></span>
<span class='c'></span>
</div>
<div id='container2'>
<span class='1'></span>
<span class='2'></span>
<span class='3'></span>
</div>
I have a method like this:
jQuery.fn.grabData = function(expr) {
return this.each(function() {
var self = $(this);
self.find("span").each(function (){
var info = $(this).attr('class');
collection += info;
});
});
};
I to run the method like this:
var collection = '';
$('#container').grabData();
$('#container2').grabData();
The collection should be adding to each other so that in the end I get this
console.log(collection);
:
abc123
But collection is undefined in the method. How can I let the method know which collection it should be adding to?
Thanks.
If you want to accumulate values for every call of the grabData function you will need a global variable:
var collection = '';
jQuery.fn.grabData = function(expr) {
return this.each(function() {
var self = $(this);
self.find('span').each(function () {
var info = $(this).attr('class');
collection += info;
});
});
};
If on the other hand you want to do it only for single call of the grabData function you could do this:
jQuery.fn.grabData = function(expr) {
var collection = '';
this.each(function() {
var self = $(this);
self.find('span').each(function () {
var info = $(this).attr('class');
collection += info;
});
});
return collection;
};
And then use it like this:
var collection = $('#container').grabData();
console.log(collection);