Getting a child property of an observableArray item to trigger a change - javascript

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())

Related

How to subscribe to variable state change using knockout.js

Here is a basic knockout.js fiddle of what I want to achieve: https://jsfiddle.net/sr3wy17t/
It does what I want to do, but not exactly in a way I want.
For completeness I will repeat parts of the above fiddle code here:
In View I've got for-each which iterates over an observableArray of items:
<div data-bind="foreach: $root.availableItems">
<div class="switchBox">
<div class="switchName"><strong data-bind="text: ' ' + name()"></strong></div>
<label class="Switch">
<input type="checkbox" data-bind="checked: state">
</label>
</div>
It iterates over elements I have in my availableItems array:
self.availableItems([
new Item(1, "item1", state1, self.onItemStateChange),
new Item(2, "item2", state2, self.onItemStateChange),
new Item(3, "item3", state3, self.onItemStateChange)
]);
as you can see, I also have a function in which i initialize each of those items with observables:
function Item(id, name, state, onChange) {
var self = this;
self.id = ko.observable(id);
self.name = ko.observable(name);
self.state = ko.observable(state);
self.state.subscribe(function(newValue) {
onChange(self, newValue);
});
}
Each of the items in an array has state variables (state1, state2, state3), which are boolean and they control which chekbox is checked and which one is not. They are (for the sake of this example) set at the beggining of ViewModel:
var state1 = true;
var state2 = false;
var state3 = false;
In reality state1, state2 and state3 are mapped from server. What I want to achieve, is after I initialize my items with starting state values, I want them to be subscribed on every change of state1, state2 and state3, so that checkbox is checked or not checked, depending on the recieved value from the server.
Currently the code in the fiddle achieves state change by accessing availableItems array like this:
setInterval(()=>{
var itemNoThatChanged=Math.floor(Math.random()*3);
var newState=Math.random()>0.5;
self.availableItems()[itemNoThatChanged].state(newState)
},1000)
The issue here, is that it's not the change in state1 or state2 or state3 that is causing the change, but rather direct access to array of availableItems....
How can I change this code, so that the change of state1, state2 and state3 causes the above behavior like in fiddle?
I need to do this with as least changes to existing code approach as possible, since it affects a lot of other stuff in the original code.
Is this possible to do, and if yes, can someone please explain how to code this in knockout.js?
Since you prefer a minimal change to your existing code;
declare your state1, state2 and state3 variables as observables.
var state1 = ko.observable(true);
var state2 = ko.observable(false);
var state3 = ko.observable(false);
Adjust your Item to accept and use these as-is instead of setting up an observable itself.
function Item(id, name, state, onChange) {
var self = this;
self.id = ko.observable(id);
self.name = ko.observable(name);
self.state = state;
self.state.subscribe(function(newValue) {
onChange(self, newValue);
});
}
The runnable example below, shows that a value change of the state1 (observable) variable (triggered from the timer callback) also affects the checkbox, without any array access.
function Item(id, name, state, onChange) {
var self = this;
self.id = ko.observable(id);
self.name = ko.observable(name);
self.state = state;
self.state.subscribe(function(newValue) {
onChange(self, newValue);
});
}
function ViewModel() {
var self = this;
var state1 = ko.observable(true);
var state2 = ko.observable(false);
var state3 = ko.observable(false);
self.availableItems = ko.observableArray([]);
self.activeItemss = ko.computed(function() {
return self.availableItems().filter(function(item) {
return item.state();
});
});
self.onItemStateChange = function(item, newValue) {
console.log("State change event: " + item.name() + " (" + newValue + ")");
};
self.init = function() {
self.availableItems([
new Item(1, "item1", state1, self.onItemStateChange),
new Item(2, "item2", state2, self.onItemStateChange),
new Item(3, "item3", state3, self.onItemStateChange)
]);
setInterval(()=>{
// Simulate a change of state1
state1(!state1());
}, 1000);
};
}
var viewModel = new ViewModel();
ko.applyBindings(viewModel);
viewModel.init();
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div data-bind="foreach: $root.availableItems">
<div class="switchBox">
<div class="switchName"><strong data-bind="text: ' ' + name()"></strong></div>
<label class="Switch">
<input type="checkbox" data-bind="checked: state">
</label>
</div>
</div>

Knockout observableArray not being populated by inherited datepicker value

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)

Knockout click event visible state

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/

"write" not working on ko.computed when adding dynamically into observable array in knockout

Consider this fiddle.
I want to add ko.computeds to a ko.observableArray dynamically:
self.items.push(ko.computed({
read: function () {
return items[i];
},
write: function (value) {
//some write action
alert(value);
}
}));
I need to manage the write function from the ko.computed into the array.
With this code, read works great, but knockout is not calling the write function, so alert is not being called.
Am I missing something? Is there a workaround for this?
I resolved it creating another ViewModel:
function item(value) {
var self = this;
self.value = ko.observable(value);
self.computed = ko.computed({
read: self.value,
write: function (value) {
alert(value);
self.value(value);
}
});
}
function header(items) {
var self = this;
self.items = ko.observableArray();
for (var i = 0; i < items.length; i++) {
self.items.push(new item(items[i]));
}
}
The HTML:
<ul data-bind="foreach: items">
<li>
<input type="text" data-bind="value: computed" />
</li>
</ul>
And the fiddle working.

KnockoutJS Select on model

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

Categories

Resources