Adding item to knock out view model , is not updating the view - javascript

I have a ViewModel which I am binding to view list item.
var MyViewModel = function() {
var self = this;
self.addItems = function(vm) {
vm.inventoryItems.push('New Item');
}
};
var myVM= new MyViewModel();
ko.applyBindings(myVM);
The view model has a property called inventoryItems (which is from a service).
I am bidning that to view using ,
<ul data-bind="foreach:inventoryItems">
<li>
<input class="form-control" type="text" data-bind="value: $data" />
</li>
</ul>
<div class="text-right">
<a data-bind="click: $parent.addItems">+ Add more</a>
</div>
Now, the items that are already in the collection , inventoryItems are getting rendered fine.
When I am adding a new item using, I can see the items being added via console, but the view is not getting updated!
self.addItems = function(vm) {
vm.inventoryItems.push('New Item');
}

The below code snippet will make your inventoryItems observable
var MyViewModel = function () {
var self = this;
self.inventoryItems = ko.observableArray();
self.addItems = function (vm) {
vm.inventories.push('New Item');
self.inventoryItems(vm.inventories);
}
};
var myVM = new MyViewModel();
ko.applyBindings(myVM);

Related

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/

Durandal 2.0 - Update the value of an observable in the view

I'm new to Durandal, my question has probably a very simple issue.
I load a list into a dropdown and the current value on the link which display the dropdown,
And the value of the link which display the dropdown is not updated correctly when an other value is selected.
But actually, I can't set the value of the observable in the select function.
View Model
var self = this;
self.system = require('durandal/system');
IPsKeys: ko.observableArray([]),
ipKeys: ko.observable(""),
activate: function (context) {
var that = this;
that.IPsKeys([]);
that.ipKeys("");
return $.when(
service.getIPSbyClientId(context.clientId).then(function (json) {
$.each(json, function (Index, Value) {
var ClientLobUWYear = {
NameLob: Value.LineOfBusiness.Name,
NameUWYear: Value.UnderwritingYear
};
that.IPsKeys().push(ClientLobUWYear);
// HERE MY VALUE IS GOOD UPDATING AND THE BINDING WORK
if (Index=== 0) {
that.ipKeys(ClientLobUWYear);
}
});
})
).then(function () {
//do some other datacontext calls for stuff used directly and only in view1
});
},
select: function (item) {
this.ipKeys = {
IdClient: item.IdClient,
IdLob: item.IdLob,
NameLob: item.NameLob,
NameUWYear: item.NameUWYear
};
/** PROBLEMS HERE **/
/** Uncaught TypeError: undefined is not a function **/
this.ipKeys(ClientLobUWYear);
},
View
<a id="select_lob-UWYear" class="dropdown-toggle" data-toggle="dropdown" href="#">
<span class="controls_value" data-bind="text: ipKeys().NameLob">ALOB</span>
<span class="controls_value" data-bind="text: ipKeys().NameUWYear">AYEAR</span>
</a>
<ul id="dropdown_year" class="dropdown-menu" data-bind="foreach: IPsKeys().sort(sortByLobYear)">
<li>
<a href="#" data-bind="click: $parent.select">
<span class="controls_value" data-bind="text: NameLob">Cargo</span>
<span class="controls_value" data-bind="text: NameUWYear">2014</span>
</a>
</li>
</ul>
Thanks a lot
The way you update an observable is like this:
var someObservable = ko.observable(""); //setting to "";
someObservable("Something else"); //updating to "Something else"
Not like this (which you are doing above)
var someObservable = ko.observable(""); //setting to "";
someObservable = "Something else";
This is overwriting someObservable with a string of value "Something else" and so is no longer an observable which is why it will not update the ui.
[JS Fiddle showing how to set observables.]

how to do data-bind for complex model knockout

I am a newbee to knockout, I'm trying to move from the MVC ViewModel binding.
I have a complex model:
SearchStudentsModel which has 2 properties
Collection of Students (Subset of students)
Number of Students overall
Note that the length of the collection isn't equal to the number overall.
I need to implement a search functionality
Student will have all the regular properties plus IsActive indicator.
I use ul and li tags to data-bind the details.
The search screen should facilitate the user in marking the active flag with an indicator (on and off) and immediately data should be saved in the database.
All the examples I referred to talk about only one level of model. I have a SearchStudent model and within that I have a collection of students.
How should the binding be for this hierarchy of models?
I have refactored your jsFiddle. Hoping you can now understand knockoutJS better. It is not your whole page/Knockout, but I think with this snippet your problem can be solved.
the markup:
<button id="searchEmployees" type="button" data-bind="click: search">Search</button>
<li data-bind="foreach: Employees">
ID: <span data-bind="text: Id"></span><br/>
Name: <span data-bind="text: Name"></span><br/>
Active: <span data-bind="click: ToggleActivation, text: IsActive"></span> <-- click<br/>
</li>
<span data-bind="text: Employees().length"></span> of
<span data-bind="text: AllEmployees().length"></span>
the js/viewmodel
function Employee(id, name, isactive){
var self = this;
self.IsActive = ko.observable(isactive);
self.Id = ko.observable(id);
self.Name = ko.observable(name);
self.ToggleActivation = function () {
if(self.IsActive() === true)
self.IsActive(false);
else
self.IsActive(true);
};
}
var searchEmployeeViewModel = function () {
var self = this;
self.Employees = ko.observableArray([]);
self.AllEmployees = ko.observableArray([]);
self.search = function () {
//Ajax call to populate Employees - foreach on onsuccess
var employee1 = new Employee(2, "Jane Doe", true);
var employee2 = new Employee(3, "Kyle Doe", false);
var employee3 = new Employee(4, "Tyra Doe", false);
var employee = new Employee(1, "John Doe", true);
self.AllEmployees.push(employee);
self.AllEmployees.push(employee1);
self.AllEmployees.push(employee2);
self.AllEmployees.push(employee3);
self.Employees.push(employee);
}
}
$(document).ready(function () {
ko.applyBindings(new searchEmployeeViewModel());
});
or you can simply use my jsFiddle if you do not like reading my code here ;)

Backbone.js event doubts

i have to create events using backbone.js.Below is my js code
var Trainee = Backbone.Model.extend();
var TraineeColl = Backbone.Collection.extend({
model: Trainee,
url: 'name.json'
});
var TraineeView = Backbone.View.extend({
el: "#area",
template: _.template($('#areaTemplate').html()),
render: function() {
this.model.each(function(good){
var areaTemplate = this.template(good.toJSON());
$('body').append(areaTemplate);
},this);
return this;
}
});
var good = new TraineeColl();
var traineeView = new TraineeView({model: good});
good.fetch();
good.bind('reset', function () {
$('#myButtons').click(function() {
traineeView.render();
});
});
<div class = "area"></div>
<div class="button" id="myButtons">
<button class="firstbutton" id="newbutton">
Display
</button>
</div>
<script id="areaTemplate" type="text/template">
<div class="name">
<%= name %>
</div>
<div class="eid">
<%= eid %>
</div>
<div class="subdomain">
<%= subdomain %>
</div>
my o/p on clicking display button is
Display // this is a button//
Sinduja
E808514
HPS
Shalini
E808130
HBS
Priya
E808515
HSG
Now from the view i have to bind a change event to the model..the changes in the model must be triggered on the view to display the output on the click of display button.
This isn´t exactly answering your queston but:
if trainee (I've renamed it to trainees) is a collection you should set it using:
new TraineeView({collection: trainees});
Then in render:
this.collection.models.each(function(trainee)
And you propably wan´t to move the call to fetch outside the view, in the router perhaps:
trainees = new TraineeColl();
view = new TraineeView({collection: trainees});
trainees.fetch();
That way your view only listens to the model.
You also should move the bind part to the views initialize method
this.collection.bind('reset', function () {
this.render();
});
Hope this helps.
var TraineeView = Backbone.View.extend({
el: "#area",
initialize : function(options){ // you will get the passed model in
//options.model
var trainee = new TraineeColl();
trainee.fetch();
trainee.bind('reset change', this.render,this); //change will trigger render
// whenever any model in the trainee collection changes or is modified
}
template: _.template($('#areaTemplate').html()),
render: function() {
this.model.each(function(trainee){
var areaTemplate = this.template(trainee.toJSON());
$('body').append(areaTemplate);
},this);
return this;
}
});
var traineeView = new TraineeView({model: trainee});
});

Categories