How to process binding : foreach after altering data in observableArray - javascript

I am altering an observableArray, modifying some data in a subscribe event. First I am converting the ObservableArray using ko.toJS(), mapping trough the data, and altering. At the end I call self.menuCategories(jsArray) to set the observableArray again.
It seems like I lose the "connection" to the observableArray in some way, since the foreach statement in my code suddenly breaks.
Either there is a very much easier way to handle this, or I am not handling the observables correctly.
CODE :
var MenuWizardModel = function() {
var self = this;
self.menuCategories = ko.observableArray();
self.commonDiscount = ko.observable(0);
// Handling adding items to menuCategories.
self.addNewSubMenuItem = function () {
var newSubMenuItem = new SubMenuItemViewModel(self.newSubMenuItemName(), []);
self.menuCategories.push(newSubMenuItem);
self.newSubMenuItemName(null);
self.createNewSubMenu(false);
}
function SubMenuItemViewModel(name, foodItemList) {
var self = this;
self.name = ko.observable(name);
self.foodItemList = ko.observableArray(foodItemList);
}
self.commonDiscount.subscribe(function(val) {
var discount = parseInt(val) / 100;
var jsArray = ko.toJS(self.menuCategories);
console.log(jsArray)
jsArray = ko.toJS(jsonArray[0].foodItemList.map(item => {
item.price = parseInt(item.price) - (parseInt(item.price) * discount);
return item;
}));
self.menuCategories(jsArray);
});
MARKUP :
<div data-bind="foreach: menuCategories">
<h4 data-bind="text: name"></h4>
<div data-bind="foreach: foodItemList" class="list-group">
...
DATA :

I think the best way to handle this type of thing is to add a computed observable to the fooditem that captures the global discount and calculates the discounted price.
something like the following.
var MenuWizardModel = function() {
var self = this;
self.menuCategories = ko.observableArray([{
name: 'Main Meals'
}]);
self.commonDiscount = ko.observable(0);
self.newSubMenuItemName = ko.observable();
// Handling adding items to menuCategories.
self.addNewSubMenuItem = function() {
var newSubMenuItem = new SubMenuItemViewModel(self.newSubMenuItemName(), [{name: 'Oranges', price: 3.99}]);
self.menuCategories.push(newSubMenuItem);
self.newSubMenuItemName(null);
//self.createNewSubMenu(false);
}
function mapFoodItem(item){
item.discountedPrice= ko.pureComputed(function(){
var discount = parseInt(self.commonDiscount()) / 100
return parseInt(item.price) - (parseInt(item.price) * discount);
});
return item;
}
function SubMenuItemViewModel(name, foodItemList) {
var self = this;
self.name = ko.observable(name);
self.foodItemList = ko.observableArray(foodItemList.map(mapFoodItem));
}
};
ko.applyBindings(new MenuWizardModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<label>Discount <input data-bind="value: commonDiscount"></label>
<label>Sub Menu Name: <input data-bind="value: newSubMenuItemName" /></label>
<button data-bind="click: addNewSubMenuItem">Add Sub Menu</button>
<div data-bind="foreach: {data: menuCategories, as: 'menu' }">
<h4 data-bind="text: menu.name"></h4>
<div data-bind="foreach: {data: menu.foodItemList, as: 'food'}" class="list-group">
<div class="list-group-item">
Name: <span data-bind="text: food.name"></span>
Price: <span data-bind="text: food.price"></span>
Discounted Price: <span data-bind="text: food.discountedPrice"></span>
</div>
</div>
</div>

Related

Knockout JS radio button binding in a for each loop

I'm unable to get the radio button checked even with the text matching the radio button value.
`
<div class="well pull-left clearfix" style="margin:5px;width:90px;padding:5px;" data-bind="click: $root.select_litho_layer_definition, css: {'background-highlight': $root.current_litho_layer_definition() === $data}">
<p class="text-center tight-padding no-margin"><strong data-bind="text: layer_name"></strong></p>
<div class="btn-group" style="width:90px">
<button class="btn btn-mini dropdown-toggle" data-toggle="dropdown" style="width:90px">
<span data-bind="text: 'Rev.' + revision"></span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu small-list" data-bind="foreach: $root.litho_layer_name_definitions()[_.indexOf($root.litho_layer_names(), layer_name)]" style="width:90px">
<li><a data-bind="text: 'Rev.' + revision, click: $root.litho_layer_name_select_revision"></a></li>
</ul>
</div>
<!-- ko foreach: exposure_array_names-->
<input type="radio" name="ExposureGroup" value="$parent.layer_name+$data" data-bind="click: $root.litho_layer_name_select_exposure, checked:$root.litho_layer_exposure"/>
<span data-bind="text: $data"></span><br>
<!--/ko-->
</div>
<!-- /ko -->
`
Each of the litho_layer_name_selected_definitions has an array of exposures called exposure_array_names
Now object structure looks like this.
{
"litho_layer_definition_sk": 90000026426,
"exposure_array_names": [
"IF1_EDGE",
"Combined"
],
"layer_name": "3HG",
"revision": 0
}
In the js, I have created an observable object of litho_layer_exposure
self.litho_layer_exposure = ko.observable(true);
self.litho_layer_exposure = ko.computed(function() {
var maskset = self.maskset();
if (!maskset) return [];
var defs = maskset.litho_layer_definitions;
if (_.isEmpty(defs)) return [];
var name = self.current_litho_layer_name();
if (!name) return [];
var revision = self.current_litho_layer_revision();
if (!_.isNumber(revision)) return;
_.reduce(defs, function(memo, d) {
if (d.layer_name === name && d.revision === revision){
memo.push(_.last(d.exposure_array_names));
}
return memo;
}, []);
});
self.litho_layer_exposure.subscribe(function(exposure) {
if (!_.isEmpty(exposure)) self.current_litho_layer_exposure(_.last(exposure));
});
I tried making the below change, which didn't work either
self.litho_layer_exposure = ko.computed({
read: function () {
var maskset = self.maskset();
if (!maskset) return [];
var defs = maskset.litho_layer_definitions;
if (_.isEmpty(defs)) return [];
var name = self.current_litho_layer_name();
if (!name) return [];
var revision = self.current_litho_layer_revision();
if (!_.isNumber(revision)) return;
/*var exposure = self.current_litho_layer_exposure();
if (!exposure) return [];*/
return _.reduce(defs, function(memo, d) {
if (d.layer_name === name && d.revision === revision) memo.push(d.default_exposure_name);
return memo;
}, []);
},
write: function (value) {
//update your self.chosenAge().population value here
self.current_litho_layer_exposure(value);
self.litho_layer_exposure(value);
},
owner: self
});
self.litho_layer_exposure.subscribe(function(exposure) {
if (!_.isEmpty(exposure)) self.current_litho_layer_exposure(_.last(exposure));
});
I´m pretty sure the initial error is
value="$parent.layer_name+$data"
which should lead to all inputs have literally "$parent.layer_name+$data" as value
you probably wanted to use a databinding to fill the attribute
data-bind="attr:{value: $parent.layer_name+$data}, checked: ..."

Bindings doesn't work on nested template loaded by JSON in KnockoutJS

Yesterday I make this question:
How can I refresh or load JSON to my viewModel on Knockout JS with complex models
Everything works OK with the fixes but when I try to use a complex json to load in the viewModel some of the buttons (specifically on Groups) doesn't work.
To resume the problem. I have a json with the previous serialized data. I use that json to fill the viewModel, this works, load correctly the data but the problem are in the "group" template, because the data is loaded but the buttons doesn't work, the only button which is working is the "remove group".
(Please refer to the image)
Any idea to fix this? Thanks.
Jsfiddle example with the problem
http://jsfiddle.net/y98dvy56/26/
!Check this picture.
The red circles indicates the buttons with problems.
The green circles indicates the buttons without problems.
Here is the body html
<div class="container">
<h1>Knockout.js Query Builder</h1>
<div class="alert alert-info">
<strong>Example Output</strong><br/>
</div>
<div data-bind="with: group">
<div data-bind="template: templateName"></div>
</div>
<input type="submit" value="Save" data-bind="click: Save"/>
</div>
<!-- HTML Template For Conditions -->
<script id="condition-template" type="text/html">
<div class="condition">
<select data-bind="options: fields, value: selectedField"></select>
<select data-bind="options: comparisons, value: selectedComparison"></select>
<input type="text" data-bind="value: value"></input>
<button class="btn btn-danger btn-xs" data-bind="click: $parent.removeChild"><span class="glyphicon glyphicon-minus-sign"></span></button>
</div>
</script>
<!-- HTML Template For Groups -->
<script id="group-template" type="text/html">
<div class="alert alert-warning alert-group">
<select data-bind="options: logicalOperators, value: selectedLogicalOperator"></select>
<button class="btn btn-xs btn-success" data-bind="click: addCondition"><span class="glyphicon glyphicon-plus-sign"></span> Add Condition</button>
<button class="btn btn-xs btn-success" data-bind="click: .addGroup"><span class="glyphicon glyphicon-plus-sign"></span> Add Group</button>
<button class="btn btn-xs btn-danger" data-bind="click: $parent.removeChild"><span class="glyphicon glyphicon-minus-sign"></span> Remove Group</button>
<div class="group-conditions">
<div data-bind="foreach: children">
<div data-bind="template: templateName"></div>
</div>
</div>
</div>
</script>
<!-- js -->
<script src="js/vendor/knockout-2.2.1.js"></script>
<script src="js/vendor/knockout-mapping.js"></script>
<script src="js/condition.js"></script>
<script src="js/group.js"></script>
<script src="js/viewModel.js"></script>
<script>
window.addEventListener('load', function(){
var json =
{"group":{"templateName":"group-template","children":[{"templateName":"condition-template","fields":["Points","Goals","Assists","Shots","Shot%","PPG","SHG","Penalty Mins"],"selectedField":"Points","comparisons":["=","<>","<","<=",">",">="],"selectedComparison":"=","value":0,"text":"Points = 0"},{"templateName":"condition-template","fields":["Points","Goals","Assists","Shots","Shot%","PPG","SHG","Penalty Mins"],"selectedField":"Points","comparisons":["=","<>","<","<=",">",">="],"selectedComparison":"=","value":0,"text":"Points = 0"},{"templateName":"condition-template","fields":["Points","Goals","Assists","Shots","Shot%","PPG","SHG","Penalty Mins"],"selectedField":"Points","comparisons":["=","<>","<","<=",">",">="],"selectedComparison":"=","value":0,"text":"Points = 0"},{"templateName":"group-template","children":[{"templateName":"condition-template","fields":["Points","Goals","Assists","Shots","Shot%","PPG","SHG","Penalty Mins"],"selectedField":"Points","comparisons":["=","<>","<","<=",">",">="],"selectedComparison":"=","value":0,"text":"Points = 0"},{"templateName":"condition-template","fields":["Points","Goals","Assists","Shots","Shot%","PPG","SHG","Penalty Mins"],"selectedField":"Points","comparisons":["=","<>","<","<=",">",">="],"selectedComparison":"=","value":0,"text":"Points = 0"},{"templateName":"condition-template","fields":["Points","Goals","Assists","Shots","Shot%","PPG","SHG","Penalty Mins"],"selectedField":"Points","comparisons":["=","<>","<","<=",">",">="],"selectedComparison":"=","value":0,"text":"Points = 0"}],"logicalOperators":["AND","OR"],"selectedLogicalOperator":"AND","text":"(Points = 0 AND Points = 0 AND Points = 0)"}],"logicalOperators":["AND","OR"],"selectedLogicalOperator":"AND","text":"(Points = 0 AND Points = 0 AND Points = 0 AND (Points = 0 AND Points = 0 AND Points = 0))"},"text":"(Points = 0 AND Points = 0 AND Points = 0 AND (Points = 0 AND Points = 0 AND Points = 0))"};
var vm = new QueryBuilder.ViewModel();
ko.mapping.fromJS(json.group, {}, vm.group);
ko.applyBindings(vm);
}, true);
</script>
Condition.js:
window.QueryBuilder = (function(exports, ko){
function Condition(){
var self = this;
self.templateName = 'condition-template';
self.fields = ko.observableArray(['Points', 'Goals', 'Assists', 'Shots', 'Shot%', 'PPG', 'SHG', 'Penalty Mins']);
self.selectedField = ko.observable('Points');
self.comparisons = ko.observableArray(['=', '<>', '<', '<=', '>', '>=']);
self.selectedComparison = ko.observable('=');
self.value = ko.observable(0);
}
exports.Condition = Condition;
return exports;
})(window.QueryBuilder || {}, window.ko);
Group.js
window.QueryBuilder = (function(exports, ko){
var Condition = exports.Condition;
function Group(){
var self = this;
self.templateName = 'group-template';
self.children = ko.observableArray();
self.logicalOperators = ko.observableArray(['AND', 'OR']);
self.selectedLogicalOperator = ko.observable('AND');
// give the group a single default condition
self.children.push(new Condition());
self.addCondition = function(){
self.children.push(new Condition());
};
self.addGroup = function(){
self.children.push(new Group());
};
self.removeChild = function(child){
self.children.remove(child);
};
}
exports.Group = Group;
return exports;
})(window.QueryBuilder || {}, window.ko);
ViewModel.js
window.QueryBuilder = (function(exports, ko){
var Group = exports.Group;
function ViewModel() {
var self = this;
self.group = ko.observable(new Group());
self.load = function (data) {
ko.mapping.fromJS(data, self);
}
self.Save = function () {
console.log(ko.toJSON(self));
}
}
exports.ViewModel = ViewModel;
return exports;
})(window.QueryBuilder || {}, window.ko);
Your issue is caused by the fact that the mapping plugin makes your data observable, but doesn't augment your data with the functions in your model such as the add, remove, etc... functions. If you do a console log for the json data when it's inserted into the view model you will notice that the data is observable but the functions are missing. You need to provide a mapping to customize your Group, Condition, etc.. constructors. Because the children array in your case is of mixed types (condition or group) Here is a custom mapping to take care of that:
var childrenMapping = {
'children': {
create: function(options) {
var data = options.data;
console.log(data);
var object;
switch(data.templateName) {
case 'condition-template':
object = new QueryBuilder.Condition(data);
break;
case 'group-template':
object = new QueryBuilder.Group(data);
break;
}
return object;
}
}
};
Then you simply need to provide this mapping in your initial mapping
ko.mapping.fromJS(json.group, childrenMapping, vm.group);
Then inside the constructor of the Group object:
function Group(data){
var self = this;
self.templateName = 'group-template';
...
ko.mapping.fromJS(data, childrenMapping, this);
}
You also need to update the Condition constructor to accept the data provided by the mapping, but since conditions don't have children you do not need to provide the childrenMapping here:
function Condition(data){
var self = this;
self.templateName = 'condition-template';
...
ko.mapping.fromJS(data, {}, this);
}
I've the mapping at the end of both function so that the mapped values override you initial value.
The updated jsfiddle here:
http://jsfiddle.net/omerio/y98dvy56/32/
This answer is related:
knockout recursive mapping issue

Minimise memory usage of empty observableArrays in knockout

I have a simple webpage with a large list of products (20,000+). When you can click on a product, it will load (via AJAX) a list of colors and display them inline. Html...
<div data-bind="foreach: products">
<span data-bind="click: $root.loadColors($data), text: $name"></span>
<ul data-bind="foreach: colors">
<li data-bind="text:$data" />
</ul
</div>
Shop view model:
function shopViewModel()
{
var self = this;
self.products = ko.observableArray([]);
self.loadColors = function(product)
{
var data = GetColorsByAjax();
product.colors(data);
}
}
Product view Model:
function productModel(data)
{
var self = this;
self.name = data.name;
self.colors = ko.observableArray([]);
}
When I have 20,000+ products, it uses a lot of memory. Each product has a colors array, which is always empty/null, until the user clicks on it, but it still uses a lot of memory.
Ideally, I'd like to remove the colors observableArray and somehow create it dynamically when user clicks on the product. Or separate it into a new viewModel.
I want to eliminate the empty observableArrays to minimise memory, but can't figure out how it do it.
I would use one of Knockout's control-flow bindings (if, with) to only bind the colors:foreach when there is actually a colors property on the productModel().
HTML:
<div data-bind="foreach: products">
<span data-bind="click: $root.loadColors($data), text: $name"></span>
<div data-bind="if: hasColors">
<ul data-bind="foreach: colors">
<li data-bind="text:$data" />
</ul>
</div>
</div>
Product View Model:
function productModel(data)
{
var self = this;
self.name = data.name;
self.hasColors = ko.observable(false);
self.colors = null;
}
Shop View Model
function shopViewModel()
{
var self = this;
self.products = ko.observableArray([]);
self.loadColors = function(product)
{
var data = GetColorsByAjax();
if(product.colors == null) {
product.colors = ko.observableArray(data);
product.hasColors(true);
} else {
product.colors(data);
}
}
}
You don't have to store an empty observable array: you can default to undefined and Knockout will treat it as an empty array in a foreach binding.
Here's a demonstration: http://jsfiddle.net/zm62T/

Find <span> value after ko.applyBindings

I'm getting some values from webapi using knockout.js and then result of that (holded in span) I'm trying to use in the other place (input in table row). Result I'm showing this way:
<h3 data-bind="foreach: book">
<span data-bind="text: Hotel" class="label label-info"/>
<span data-bind="text: Номер" class="label label-info"/>
<span class="label label-info" data-bind=" text: Фамилия"/>
<span class="label label-info ad" data-bind=" text: Колчел"/>
<span class="label label-info ch" data-bind=" text: Дети"/>
</h3>
and this is knockout code:
<script>
function BookViewModel(baseUri) {
var self = this;
self.Номер = ko.observable("");
self.Колчел = ko.observable("");
self.Дети = ko.observable("");
self.Фамилия = ko.observable("");
self.Hotel = ko.observable("");
var book = {
Номер: self.Номер,
Колчел: self.Колчел,
Дети: self.Дети,
Фамилия: self.Фамилия,
Hotel: self.Hotel
};
self.book = ko.observable();
self.books = ko.observableArray();
$.getJSON(baseUri, self.book);
}
$(document).ready(function () {
var url = location.href.split("/")
var baseUri;
if (url[4].toString = 'x') {
baseUri = '/api/xTourist/' + url[5];
}
else if (url[4].toString = 'y') {
baseUri = '/api/yTourist/' + url[5];
}
ko.applyBindings(new BookViewModel(baseUri));
//This is how I'm trying to read result and use this result in input field in the other table.
var ad = $("span.ad").val();
var ch = $("span.ch").val();
$("#gvOrders tr input.pax_ad").each(function () {
$(this).val(ad);
});
$("#gvOrders tr input.pax_ch").each(function () {
$(this).val();
});
});
</script>
Unfortunately this var is undefrined. I'm really don't understand why values cannot be readed after ko already apply binding.
The knockout documentation is really good. I suggest you start there to help get a better understanding of how knockout works. It'll really save you some time and frustration.
As for the $.getJSON call, the documentation has lots of information and examples.

If binding statement in knockout with view page

I have this div which displays a letter, but I want to add an if statement of when to show this div based on the following condition:
if usersCount() > 3 then show letter
<div class=" header" id="letter" data-bind="text: Letter">
....
</div>
How could i add the if statement along with the text - expression statement?
data-bind="if: UserCount() > 13 then text:Letter"` ....??
var userViewModel = function (data) {
var _self = this;
_self.Letter = ko.observable(data.Letter);
};
var roleViewModel = function (data) {
var _self = this;
_self.UserCount = ko.observable(data.UserCount);
};
Check out the Visible Binding. You'll want to create a property in you View Model to handle the logic of hiding/showing the div. Here is a JSFiddle to demonstrate.
<div data-bind="visible: shouldShowMessage, text: Letter">
</div>
<script type="text/javascript">
var viewModel = function(){
var self = this;
self.Letter = ko.observable('Hello, World!');
self.UserCount = ko.computed(function() {
return Math.floor((Math.random() * 20) + 1);
});
self.shouldShowMessage = ko.computed(function() {
return (self.UserCount() > 13);
});
};
</script>

Categories