I'm new to Knockout js and I found an issue in button click event. I have a list where each list item has a button for comment. When I click the button, the invisible comment box should be visible. Following is my HTML code:
<ul class="unstyled list" data-bind="foreach: filteredItems">
<li>
<input type="checkbox" value="true" data-bind =" attr: { id: id }" name="checkbox" class="checkbox">
<label class="checkbox-label" data-bind="text: title, attr: { for: id }"></label>
<button class="pull-right icon" data-bind="click: loadComment, attr: { id: 'btn_' + id }"><img src="../../../../../Content/images/pencil.png" /></button>
<div class="description" data-bind="visible: commentVisible, attr: { id : 'item_' + id}">
<textarea data-bind="value: comment" class="input-block-level" rows="1" placeholder="Comment" name="comment"></textarea>
<div class="action">
<button class="accept" data-bind="click: addComment">
<img src="../../../../../Content/images/accept.png" /></button>
<button class="cancel" data-bind="click: cancel">
<img src="../../../../../Content/images/cancel.png" /></button>
</div>
</div>
</li>
</ul>
In my view model, I have mentioned when click the loadComment the comment should be visible
var filteredItems = ko.observableArray([]),
filter = ko.observable(),
items = ko.observableArray([]),
self = this;
self.commentVisible = ko.observable(false);
self.comment = ko.observable();
self.addComment = ko.observable(true);
self.cancel = ko.observable();
self.loadComment = function (item) {
self.commentVisible(true);
}
The problem is when I click the loadComment button, all the comment boxes in each list items getting visible. I want to make only the clicked button's comment box should be appear.
Need some help.
Thanks
You declaration doesnt make much sense to me. commentVisible is not a property of filteredItems so when doing a foreach, it will not be accessible unless you use the $parent binding. FilteredItems itself is a private variable and will not be exposed to the viewmodel and that should cause the binding to fail. I would look at the error console to see if that gives any clues.
Here is what I did to make a somewhat working example (note that this uses parent binding and is probably not what you are going for):
var VM = (function() {
var self = this;
self.filteredItems = ko.observableArray([{id: 1, title: 'Test'}]);
self.filter = ko.observable();
self.items = ko.observableArray([]);
self.commentVisible = ko.observable(false);
self.comment = ko.observable();
self.addComment = ko.observable(true);
self.cancel = function(){
self.commentVisible(false);
};
self.loadComment = function (item) {
self.commentVisible(true);
}
return self;
})();
ko.applyBindings(VM);
http://jsfiddle.net/infiniteloops/z93rN/
Knockout binding contexts: http://knockoutjs.com/documentation/binding-context.html
What you probably want to do it to create a filtered item object with those properties that are referenced within the foreach and populate the filteredItems obeservable array with them.
That might look something like this:
var FilteredItem = function(id,title){
var self = this;
self.id = id;
self.title = title;
self.commentVisible = ko.observable(false);
self.comment = ko.observable();
self.addComment = ko.observable(true);
self.cancel = function(){
self.commentVisible(false);
};
self.loadComment = function (item) {
self.commentVisible(true);
}
}
var VM = (function() {
var self = this;
var item = new FilteredItem(1, 'Test');
self.filteredItems = ko.observableArray([item]);
self.filter = ko.observable();
self.items = ko.observableArray([]);
return self;
})();
ko.applyBindings(VM);
http://jsfiddle.net/infiniteloops/z93rN/2/
Related
I am having trouble populating an observableArray with a value inherited from a datepicker. I have a disabled textbox that displays the value of the datepicker as part of the data collection section. As it is disabled and not being typed in, it is not updating the observableArray.
I have created an example jsfiddle where I have stripped down and localised the problem.
Any help getting the value to appear in the observableArray would be great as I am really struggling to figure this one out!
HTML
<!--Date Load -->
<span><b>Select a date:</b></span>
<span><input id="theDate" data-bind="datepicker: viewModelWardStaff.dateMonthYear, datepickerOptions: { dateFormat: 'dd/mm/yy' } "></span>
<!--Input Form -->
<span><h4>Input New Entries</h4></span>
<div style="border: solid 1px;" data-bind="with: viewModelWardStaff">
<form class="grid-form" id="dataCollection">
<fieldset>
<div data-row-span="1">
<div data-field-span="1">
<label>Date</label>
<input id="cDate" class="autosend" data-bind="textInput: dateMonthYear, enable: false">
</div>
<div data-field-span="1">
<label>Status</label>
<input id="cStatus" maxlength="200" class="autosend" data-bind="textInput: wardstaff.Status" type="text">
</div>
</div>
</fieldset>
<div style="margin: 5px;">
<a style="margin-left: 300px;" id="addFileButton" class="button-link" data-bind="click: viewModelWardStaff.addEntry">Add</a>
</div>
</form>
</div>
<h4>View Model Ward Staff</h4>
<div data-bind="with: viewModelWardStaff">
<pre data-bind="text: ko.toJSON($data, null, 2)"></pre>
</div>
KnockoutJS
moment.locale('en-gb');
function WardStaff(data) {
var self = this;
self.Date = ko.observable(data.Date());
self.Status = ko.observable(data.Status());
};
function oWardStaff() {
var self = this;
self.Date = ko.observable();
self.Status = ko.observable();
};
var viewModelWardStaff = function () {
var self = this;
self.wardstaff = new oWardStaff();
self.dateMonthYear = ko.observable();
self.entries = ko.observableArray([]);
self.addEntry = function () {
self.entries.push(new WardStaff(self.wardstaff));
}
self.removeEntry = function (entry) {
self.entries.remove(entry);
}
};
// dateString knockout
ko.bindingHandlers.dateString = {
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
var value = valueAccessor(),
allBindings = allBindingsAccessor();
var valueUnwrapped = ko.utils.unwrapObservable(value);
var pattern = allBindings.datePattern || 'YYYY-MM-DD HH:mm:ss';
if (valueUnwrapped == undefined || valueUnwrapped == null) {
$(element).text("");
}
else {
var date = moment(valueUnwrapped, "YYYY-MM-DDTHH:mm:ss"); //new Date(Date.fromISO(valueUnwrapped));
$(element).text(moment(date).format(pattern));
}
}
}
//datepicker knockout
ko.bindingHandlers.datepicker = {
init: function (element, valueAccessor, allBindingsAccessor) {
//initialize datepicker with some optional options
var options = allBindingsAccessor().datepickerOptions || {};
$(element).datepicker(options);
//WORK
//handle the field changing
ko.utils.registerEventHandler(element, "change", function () {
var observable = valueAccessor();
if (moment($(element).datepicker("getDate")).local().format('YYYY-MM-DD') == 'Invalid date') {
observable(null);
}
else {
observable(moment($(element).datepicker("getDate")).local().format('YYYY-MM-DD'));
}
});
//handle disposal (if KO removes by the template binding)
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
$(element).datepicker("destroy");
});
},
//update the control when the view model changes
update: function (element, valueAccessor) {
var value = ko.utils.unwrapObservable(valueAccessor());
current = $(element).datepicker("getDate");
if (moment(value).format('DD/MM/YYYY') == 'Invalid date') {
$(element).datepicker("setDate", null);
}
}
};
// Master View Model
var masterVM = function () {
var self = this;
self.viewModelWardStaff = new viewModelWardStaff();
};
// Activate Knockout
ko.applyBindings(masterVM);
I think the problem is that your observable date, dateMonthYear, lives in your master view model. The Date property of self.wardstaff is never set.
You could solve this by sharing the observable in your master view model with the one in the wardstaff property:
function oWardStaff(obsDate) {
var self = this;
self.Date = obsDate;
self.Status = ko.observable();
};
/* ... */
self.dateMonthYear = ko.observable();
self.wardstaff = new oWardStaff(self.dateMonthYear);
Now, whenever you pick a new date, it writes it to the observable referenced by both viewmodels.
This line suddenly becomes useful:
function WardStaff(data) {
var self = this;
self.Date = ko.observable(data.Date()); // <-- here
self.Status = ko.observable(data.Status());
};
since Date is now actually set.
Fiddle that I think now works correctly: https://jsfiddle.net/n0t91sra/
(let me know if I missed some other desired behavior)
I'm developing a 3 part upload form, where users can upload 3 sets of files
So far, I've got the following viewModel
var FileGroupViewModel = function (id) {
var self = this;
self.id = ko.observable(id);
self.files = ko.observableArray();
self.removeFile = function (item) {
self.files.remove(item);
}
self.fileUpload = function (data, e) {
var file = e.target.files[0];
self.files.push(file);
};
}
var ViewModel = function () {
var self = this;
self.fileGroups = ko.observableArray();
self.getFileGroupById = function (id) {
ko.utils.arrayFilter(self.fileGroups(), function (item) {
return item.id == id;
});
};
self.uploadFiles = function () {
alert('Uploading');
}
}
var viewModel = new ViewModel();
viewModel.fileGroups.push(new FileGroupViewModel(1));
viewModel.fileGroups.push(new FileGroupViewModel(2));
viewModel.fileGroups.push(new FileGroupViewModel(3));
ko.applyBindings(viewModel);
I have 3 'groups' of files a user can upload to.
(I will do the actual upload functionality later)
I'm struggling with how to bind my row to a specific item of the array?
Maybe I shouldn't use an observable array?
<div class="row files" id="files1" data-bind="???">
<h2>Files 1</h2>
<span class="btn btn-default btn-file">
Browse <input data-bind="event: {change: fileUpload}" type="file" />
</span>
<br />
<div class="fileList" data-bind="foreach: files"> <span data-bind="text: name"></span>
Remove
</div>
</div>
The idea is when a user selects files, they appear in a list under the button:
..with a link to remove the file from the upload queue.
I've set up a fiddle here - https://jsfiddle.net/alexjamesbrown/c9fvzjte/
There are few important modifications required to make your code work independently across files 0,1,2
KeyNote
event: { change: function(){fileUpload($data,$element.files[0])}}
here we are passing our selected file i.e filedata using $element in
change event not in usual click event . Filedata will have complete file information .
view:
<div class="row files" id="files1" data-bind="foreach:fileGroups">
<h2>Files 0</h2>
<span class="btn btn-default btn-file">
Browse <input data-bind="event: { change: function() { fileUpload($data,$element.files[0]) } }" type="file" />
</span>
<div class="fileList" data-bind="foreach: files"> <span data-bind="text: name"></span>
Remove
</div>
viewModel:
var SubFunction = function (data) {
var self = this;
self.name = data.name;
self.removeFile = function (item1) {
item1.files.remove(this); //current reference data & item1 has parent reference data
}
}
var FileGroupViewModel = function (id) {
var self = this;
self.id = ko.observable(id);
self.files = ko.observableArray([new SubFunction({
'name': 'Test'
})]);
self.fileUpload = function (item1, item2) {
self.files.push(new SubFunction(item2));
};
}
var ViewModel = function () {
var self = this;
self.fileGroups = ko.observableArray();
self.getFileGroupById = function (id) {
ko.utils.arrayFilter(self.fileGroups(), function (item) {
return item.id == id;
});
};
self.uploadFiles = function () {
alert('Uploading');
}
}
var viewModel = new ViewModel();
viewModel.fileGroups.push(new FileGroupViewModel(1));
ko.applyBindings(viewModel);
working sample up for grabs here
Working sample if you are planning to reuse your Html
I'm new to Knockout and I'm building an app that's effectively a large-scale calculator. So far I have two instances of knockout running on one page. One instance is working perfectly fine, however the other one is entirely broken and just won't seem to register at all?
Below is my Javascript, fetchYear is the function that works perfectly fine and fetchPopulation is the one that's completely broken. It doesn't seem to register "ageview" from the HTML at all and I can't figure out.
The error:
Uncaught ReferenceError: Unable to process binding "foreach: function
(){return ageView }" Message: ageView is not defined
Thanks in advance.
JS:
var index = {
fetchYear: function () {
Item = function(year){
var self = this;
self.year = ko.observable(year || '');
self.chosenYear = ko.observable('');
self.horizon = ko.computed(function(){
if(self.chosenYear() == '' || self.chosenYear().horizon == undefined)
return [];
return self.chosenYear().horizon;
});
};
YearViewModel = function(yeardata) {
var self = this;
self.yearSelect = yeardata;
self.yearView = ko.observableArray([ new Item() ]);
self.add = function(){
self.yearView.push(new Item("New"));
};
};
ko.applyBindings(new YearViewModel(yearData));
},
fetchPopulation: function () {
popItem = function(age){
var self = this;
self.age = ko.observable(age || '');
self.chosenAge = ko.observable('');
self.population = ko.computed(function(){
if(self.chosenAge() == '' || self.chosenAge().population == undefined)
return [];
return self.chosenAge().population;
});
};
PopulationViewModel = function(populationdata) {
var self = this;
self.ageSelect = populationdata;
self.ageView = ko.observableArray([ new popItem() ]);
self.add = function(){
self.ageView.push(new popItem("New"));
};
};
ko.applyBindings(new PopulationViewModel(populationData));
}
}
index.fetchYear();
index.fetchPopulation();
HTML:
<div class="row" data-bind="foreach: yearView">
<div class="grid_6">
<img src="assets/img/index/calendar.png" width="120" height="120" />
<select class="s-year input-setting" data-bind="options: $parent.yearSelect, optionsText: 'year', value: chosenYear"></select>
<label for="s-year">Start year for the model analysis</label>
</div>
<div class="grid_6">
<img src="assets/img/index/clock.png" width="120" height="120" />
<select class="s-horizon input-setting" data-bind="options: horizon, value: horizon"></select>
<label for="s-horizon">Analysis time horizon</label>
</div>
</div>
<div class="row" data-bind="foreach: ageView">
<div class="grid_6">
<img src="assets/img/index/calendar.png" width="120" height="120" />
<select class="s-year input-setting" data-bind="options: ageSelect, optionsText: 'age', value: chosenAge"></select>
<label for="s-agegroup">Age group of <br> target population</label>
</div>
<div class="grid_6">
<img src="assets/img/index/clock.png" width="120" height="120" />
<input class="s-population input-setting"></input>
<label for="s-population">Size of your patient <br> population <strong>National</strong> </label>
</div>
</div>
When you do this (in fetchYear):
ko.applyBindings(new YearViewModel(yearData));
You are binding the entire page with the YearViewModel view model. But the YearViewModel doesn't have a property called ageView so you get the error and knockout stops trying to bind anything else.
What you need to do is restrict your bindings to cover only part of the dom by passing the element you want to ko.applyBindings. For example:
<div class="row" id="yearVM" data-bind="foreach: yearView">
//....
<div class="row" id="popVM" data-bind="foreach: ageView">
And then:
ko.applyBindings(new YearViewModel(yearData), document.getElementById("yearVM"));
//...
ko.applyBindings(new PopulationViewModel(populationData), document.getElementById("popVM"));
Now your bindings are restricted just to the part of the DOM that actually displays stuff from that model.
Another alternative is to just have your two view models as part of a parent view model and then you can apply the binding to the entire page. This makes it easier if you need to mix parts from both VMs and they are not conveniently separated in distinct sections of your page. Something like:
var myParentVM = {
yearVM : index.fetchYear(), // note, make this return the VM instead of binding it
popVM : index.fetchPopulation(), // ditto
}
ko.applyBindings(myParentVM);
And then you'd declare your bindings like so:
<div class="row" data-bind="foreach: yearVM.yearView">
The main reason why this is not working is because you call ko.applyBindings() more than once on a page (that is not really forbidden but is a bad practice in my opinion).
If you need to call it twice, you must call it with a container for which region this bind is meant to.
Something like this:
ko.applyBindings(new YearViewModel(yearData), document.getElementById('YourYearViewElementId'));
The error you get is from the first binding, which tries to process the whole page and does not find the 'ageView' in its ViewModel.
Better would be if you build a single ViewModel for a single Page where you have sub-models for sections if needed.
Some pseudo code for such a scenario:
var Section1ViewModel = function() {
var self = this;
self.property1 = ko.observable();
self.myComputed = ko.computed(function () {
// do some fancy stuff
});
self.myFunc = function() {
// do some more fancy stuff
};
}
var Section2ViewModel = function() {
var self = this;
self.property1 = ko.observable();
self.myComputed = ko.computed(function () {
// do some fancy stuff
});
self.myFunc = function() {
// do some more fancy stuff
};
}
var PageViewModel = function() {
var self = this;
self.section1 = ko.observable(new Section1ViewModel());
self.section2 = ko.observable(new Section2ViewModel());
self.myGlobalFunc = function() {
// do some even more fancy stuff
}
}
ko.applyBindings(new PageViewModel());
Hope that helps.
Best regards,
Chris
Problem
I have a shoppingcart viewmodel with an observableArray of cartitems view models.
When I update the subtotal property of my cartitems view model, a computedObservable on my shoppingcart viewmodel needs to update but I don't know how to get it to trigger the update
Example
function shoppingcart() {
var self = this;
self.cartItems = ko.observableArray([]);
self.grandTotal = ko.computed(function() {
var total = 0;
_.each(self.cartItems(), function (item) {
total += item.subTotal;
}
}
//inital load of the data
dataservice.loadCartItems(self.cartItems);
}
function cartItem() {
var self = this;
self.quantity = ko.observable(0);
self.price = 0.00;
self.subTotal = ko.computed(function() {
return self.price * self.quantity();
}
}
Then in my view I have something similar to this
<ul data-bind='foreach: cartItems'>
<!--other stuff here -->
<input type='text' data-bind="value: quantity, valueUpdate: 'afterkeydown'"/>
</ul>
<span data-bind='value: grandTotal'></span>
Is this suppose to work and I've just messed up somewhere along the line, or do I need to add something else to get this to update?
Right now the grandTotal in the span will not be updated when the quantity in the textbox is changed. I'm assuming it's because this child property doesn't actually count as the cartItems collection being changed.
What's a good way to trigger the update to the collection here?
You were not returning anything from your grandTotal computed. Also, you were trying to add the subTotal function to the running total instead of its return value. You need to invoke with parenthesis in order to invoke the computed on cartItem.
function shoppingcart() {
var self = this;
self.cartItems = ko.observableArray([]);
self.grandTotal = ko.computed(function() {
var total = 0;
_.each(self.cartItems(), function (item) {
total += item.subTotal(); // need parenthesis to invoke
}
return total; // return a value, otherwise function is void
}
//inital load of the data
dataservice.loadCartItems(self.cartItems);
}
function cartItem() {
var self = this;
self.quantity = ko.observable(0);
self.price = 0.00;
self.subTotal = ko.computed(function() {
return self.price * self.quantity();
}
}
So if I understand correctly, the main problem is that you need trigger an observableArray mutation when one of its element changes. It can be done, but I don't know if it's a best practice. See this for an alternative implementation: Observable notify parent ObservableArray
The example solution at this fiddle calls valueHasMutated manually: http://jsfiddle.net/F6D6U/6/
html:
<ul data-bind='foreach: cartItems'>
<!--other stuff here -->
<input type='text' data-bind="value: quantity, valueUpdate: 'afterkeydown'"/>
* <span data-bind="text:price"></span>
= <span data-bind="text:subTotal"></span>
<br />
</ul>
<span data-bind='text: grandTotal'></span>
js:
function cartItem(q, p, a) {
var self = this;
self.quantity = ko.observable(q);
self.price = p;
self.parentArray = a;
self.subTotal = ko.computed(function() {
var subtotal = parseFloat(self.price,10) * parseFloat(self.quantity(),10);
self.parentArray.valueHasMutated();
return subtotal;
},self);
}
function shoppingcart() {
var self = this;
self.cartItems = ko.observableArray();
self.cartItems([
new cartItem(10,100, self.cartItems),
new cartItem(1,3, self.cartItems),
]);
self.grandTotal = ko.computed(function() {
var total = 0;
ko.utils.arrayForEach(self.cartItems(), function (item) {
total += item.subTotal();
});
return total;
}, self);
//inital load of the data
//dataservice.loadCartItems(self.cartItems);
}
ko.applyBindings(new shoppingcart())
I'm trying to bind a 1-many mapping using KnockoutJS, where 1 zip code can have many 'agents'. I have the following classes:
function CaseAssignmentZipCode(zipcode, agent) {
var self = this;
self.zipcode = ko.observable(zipcode);
self.agent = ko.observable(agent);
}
function Agent(id, name) {
var self = this;
self.id = id;
self.name = name;
}
function ZipcodeAgentsViewModel() {
var self = this;
self.caseAssignmentZipCodes = ko.observableArray([]);
self.agents = ko.observableArray([]);
jdata = $.parseJSON($('#Agents').val());
var mappedAgents = $.map(jdata, function (a) { return new Agent(a.Id, a.Name) });
self.agents(mappedAgents);
var dictAgents = {};
$.each(mappedAgents, function (index, element) {
dictAgents[element.id] = element;
});
var jdata = $.parseJSON($('#CaseAssignmentZipCodes').val());
var mappedZipcodeAgents = $.map(jdata, function (za) { return new CaseAssignmentZipCode(za.ZipCode, dictAgents[za.UserId], false) });
self.caseAssignmentZipCodes(mappedZipcodeAgents);
}
var vm = new ZipcodeAgentsViewModel()
ko.applyBindings(vm);
My bindings look like this:
<table>
<thead><tr><th>Zipcode Agents</th></tr></thead>
<tbody data-bind="foreach: caseAssignmentZipCodes">
<tr>
<td><input data-bind="value: zipcode"></td>
<td><select data-bind="options: $root.agents, value: agent, optionsText: 'name'"></select></td>
<td>Remove</td>
</tr>
</tbody>
</table>
Everything binds fine the first time, with the table and select fields appearing properly. However, nothing happens when I change the selected value on any of the select elements. I have bound other elements to them and these don't update, and I've tried using .subscribe() to listen for the update event, but this doesn't fire either.
I expect there's something wrong with the way I'm setting up/binding these relationships, but I can't figure it out to save my life.
Thanks!
I think you need to add
self.agents = ko.observableArray([]);
at the top of ZipcodeUsersViewModel