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>
Related
I have the following client-side code:
var ProfileManager = function () {
self.SelectedLanguage = ko.observable();
self.SelectedLanguage.subscribe(function (newValue) {
alert("The person's new name is " + newValue);
});
var bindUIwithViewModel = function (viewModel) {
ko.applyBindings(viewModel);
};
}
and I later do bindUIwithViewModel(self);.
and a HTML select bound to SelectedLanguage:
<select id="selectAvailableLanguages" class="form-control language-select" data-bind="options: AvailableLanguages, optionsText: 'Code', value : SelectedLanguage"></select>
The select gets populated successfully, the value inside SelectedLanguage observable changes, but the alert just won't show up. Any ideas?
Also, don't know if related but the observe array inside __ko.mapping_ is an array[0]..
Please check the fiddle. Note, "AvailableLanguages" and "SelectedLanguage" properties should exist in the same context, i.e. belong the binded view model.
I've modified your JS code:
var ProfileManager = function () {
self.SelectedLanguage = ko.observable();
self.SelectedLanguage.subscribe(function (newValue) {
alert("The person's new name is " + JSON.stringify(newValue));
});
self.AvailableLanguages = [ { Code: 'c++', Person: 'John' }, { Code: 'c#', Person: 'Mark' } ]
}
ko.applyBindings(new ProfileManager());
I have a dropdown and data binded to it as below
HTML:
<div>
<label>Notice Type</label>
<select id="ntctype" data-bind="options: NoticeType, value: selectedNoticeType, optionsCaption:'Choose...', optionsValue:'NoticeTypeID', optionsText:'NoticeTypeDescription'"></select>
</div>
KO JS:
self.NoticeType = ko.observableArray([]);
self.selectedNoticeType = ko.observable();
$.getJSON("GetNoticeType", null, function (data) {
self.NoticeType(data);
}
);
the NoticeType array looks like this
[{"NoticeTypeID":1,"NoticeTypeDescription":"Close"},{"NoticeTypeID":2,"NoticeTypeDescription":"Open"}]
I would like to set the deafult value as Close after binding.I tried using optionsAfterRender & ko.applybindingstoNode none of them worked.
What would be the clean and neat approach to do this?
As mentioned in the comments you just need to set default value to value binded observable in viewModel
View Model :
function accountViewModel() {
var self = this;
self.NoticeType = ko.observableArray();
self.selectedNoticeType = ko.observable();
self.ajaxcall = function () {
self.NoticeType([{
"NoticeTypeID": 1,
"NoticeTypeDescription": "Close"
}, {
"NoticeTypeID": 2,
"NoticeTypeDescription": "Open"
}]);
self.selectedNoticeType = ko.observable(1);
}
self.ajaxcall();
}
Working fiddle here
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 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/
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