I'm having an issue using a computedObservable. They seem pretty straight forward, but I think my use case might be a little odd. The issue is when onBottom() is called, it doesn't see the items that are in the container.slides() array. Instead, I get "Undefined is not a function".
Since onTop() works fine, it makes me think that it's the relationship between Slide and FeaturedContent that is causing the issue, but I think it should work fine. Any help here would be greatly appreciated.
HTML
<div class="section-block" data-bind="foreach: slides">
<div class="row well">
<div class='control'>
<a href='#' data-bind="click: moveUp, ifnot: onTop">
<i class="fa fa-arrow-up"></i>
</a>
<a href='#' data-bind="click: moveDown, ifnot: onBottom">
<i class="fa fa-arrow-down"></i>
</a>
<span class='pull-right'>Remove</span>
</div>
<h5 data-bind="text: headline"></h5>
<p data-bind="text: image"></p>
</div>
</div>
JS
var FeaturedContent = function() {
var self = this;
self.maxSlides = 14;
self.slides = ko.observableArray([
new Slide({}, "I should be last", "someimg", 0, self),
new Slide({}, "I should be first", "anotherimg", 1, self),
new Slide({}, "I should be second-", "anotherimg", 2, self),
]);
// ... snipped.
};
var Slide = function(contentItem, headline, image, order, container) {
var self = this;
// -- Data
self.contentItem = contentItem;
self.headline = headline;
self.image = image;
self.position = ko.observable(0);
self.onBottom = ko.computed(function() {
return (self.position() === container.slides().length - 1) ? true : false;
});
self.onTop = ko.computed(function() {
return (self.position() === 0) ? true : false;
});
return self;
};
During creating
self.slides = ko.observableArray([
new Slide({}, "I should be last", "someimg", 0, self),
new Slide({}, "I should be first", "anotherimg", 1, self),
new Slide({}, "I should be second-", "anotherimg", 2, self),
]);
in the FeaturedContent your code will create new Slide and use self to get container.slides().length i.e. self is the container but slides() has not created. It's a circle reference. So container.slides() is undefined.
try something like this container.slides() && self.position() === container.slides().length - 1
Related
This is baffling. It is such a simple thing, but I'm not able to get it to work. I am adding an input field to the form on a button click (originally). At this point I'm just trying to see any value in the view (hence the simple p tag)
HTML VIEW
<span>Add secondary field</span>
<md-button class="md-fab md-mini" ng-click="vm.addVals()">
<i class="material-icons">add</i>
</md-button>
<div ng-if="moreVal">
<div data-ng-repeat="vl in valHolder.valArr track by $index">
<p>My Value: {{vl.myVal}}</p>
</div>
Controller
function EditFormController($scope, $sanitize, ngToast) {
var vm = this;
vm.addVals = addVals;
$scope.valHolder= {valArr: []};
function addVals(){
var ln = $scope.valHolder.valArr.length;
$scope.valHolder.valArr.push({myVal: 'Test'+ln});
$scope.moreVal = true;
}
}());
I have checked that valArr is being populated with new myVal values on button click. But I cannot see anything in the View. the ng-repeat div is empty. Why is this happening? I have been searching for a solution all day now, but this is so absurd no one seems to have this issue. Don't know what I'm doing wrong. I would really appreciate an answer.
Write below code in your controller:
var self = this;
self.valHolder= {valArr: []};
function addVal(){
var ln = self.valHolder.valArr.length;
self.valHolder.valArr.push({myVal: 'Test'+ln});
self.moreVal = true;
}
Write in your HTML like below:
<div ng-if="moreVal">
<div data-ng-repeat="vl in vm.valHolder.valArr track by $index">
<p>My Value: {{vl.myVal}}</p>
</div>
Your addVal function is not getting called. Please change your HTML view to this-
<span>Add secondary field</span>
<md-button class="md-fab md-mini" ng-click="addVal()">
<i class="material-icons">add</i>
</md-button>
<div ng-if="moreVal">
<div data-ng-repeat="vl in valHolder.valArr track by $index">
<p>My Value: {{vl.myVal}}</p>
</div>
and Controller code to this-
$scope.valHolder = { valArr: [] };
$scope.addVal = function () {
var ln = $scope.valHolder.valArr.length;
$scope.valHolder.valArr.push({ myVal: 'Test' + ln });
$scope.moreVal = true;
}
In case you are using ControllerAs syntax and you have specified controllerAs:"vm", then your code will be below
<span>Add secondary field</span>
<md-button class="md-fab md-mini" ng-click="vm.addVal()">
<i class="material-icons">add</i>
</md-button>
<div ng-if="vm.moreVal">
<div data-ng-repeat="vl in vm.valHolder.valArr track by $index">
<p>My Value: {{vl.myVal}}</p>
</div>
and Controller code to this-
var self = this;
self.valHolder = { valArr: [] };
self.addVal = function () {
var ln = self.valHolder.valArr.length;
self.valHolder.valArr.push({ myVal: 'Test' + ln });
self.moreVal = true;
}
You are using ng-click="vm.addVals()" and $scope in controller.
I think you should use var vm in controller as well. Add these lines:
var vm = this;
vm = {};
and
vm.addVals = addVals;
My html page is :
<div data-role="content">
<div id="menu">
<ul id="menu" data-role="listview" class="ui-listview "
data-bind="foreach: menu">
<li>
<a data-bind="text:name, attr: {href: urlmenu}"></a>
<a href="#" data-bind="{ click: $parent.remove }"
data-role="button" data-icon="delete"></a>
</li>
</ul>
</div>
</div>
<div data-role="footer" data-position="fixed">
<div data-role="navbar">
<ul id="footer">
<li>Home</li>
<li>Asignaturas</li>
<li>Mensajes</li>
</ul>
</div>
</div>
And my JS code is :
$( document ).on( "pagebeforechange" , function(e, data) {
var toPage = data.toPage[0].id;
if( toPage == "home"){
ko.cleanNode(document.getElementById('menu'));
menu();
}
});
function menuViewModel(){
var self = this;
self.menu = ko.observableArray([]);
self.menu.removeAll();
self.menu = ko.observableArray([
new EditMenuViewModel("Perfil"),
new EditMenuViewModel("Asignaturas")
]);
}
function EditMenuViewModel(name) {
this.name = ko.observable(name);
this.urlmenu = ko.observable("#"+name);
};
function menu(){
var menuviewModel = new menuViewModel();
ko.applyBindings(menuviewModel, document.getElementById('menu'));
}
When I load my page for the first time everything works fine, but when I click on link footer home, the array content is duplicated.
Example is here:
Any idea?
Thanks
You have two DOM elements with id=menu, a div and a ul.
<div id="menu"> <!-- <-- change this id for example -->
<ul id="menu" data-role="listview" class="ui-listview "
data-bind="foreach: menu">
...
</ul>
</div>
Ids should be unique, you need to change the id on one of your elements, hopefully this will also solve your problem.
Update
As you can read in this thread, ko.cleanNode will not remove items created using foreach binding.
You need to change your approach.
Here is a jsFiddle that reproduces your problem.
What you can do is stop cleaning+applying bindings, and update your observableArray instead:
$( document ).on( "pagebeforechange" , function(e, data) {
var toPage = data.toPage[0].id;
if( toPage == "home"){
menuviewModel.menu.removeAll(); //clear menu
//add whatever menu item you need
menuviewModel.menu.push(new EditMenuViewModel("New Menu1 " + (new Date()).getTime()));
menuviewModel.menu.push(new EditMenuViewModel("New Menu2 " + (new Date()).getTime()));
}
});
function menuViewModel(){
var self = this;
self.menu = ko.observableArray([]);
self.menu.removeAll();
self.menu = ko.observableArray([
new EditMenuViewModel("Perfil"),
new EditMenuViewModel("Asignaturas")
]);
}
function EditMenuViewModel(name) {
this.name = ko.observable(name);
this.urlmenu = ko.observable("#"+name);
};
//bind only once
var menuviewModel = new menuViewModel();
ko.applyBindings(menuviewModel, document.getElementById('menu'));
Here is an example
This is old thread, but I've found a (ugly) way to overcome it:
before the cleaning, I'm caching the observable array values and set it with only 1 value. After the rebind, I'm restoring the cached values. Something like this:
var self = this;
self.myArray = ko.observableArray(['val1', 'val2']);
var tempArray = [];
self.BeforeCleaning = function () {
tempArray = self.myArray()
self.myArray(['temp value']);
};
self.AfterRebinding = function () {
self.myArray(tempArray);
};
horrible, but works for me.
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
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/
In my view I am looping through an observableArray (itemGroup) that has one property that is also an observableArray (item). I have a method to remove an entire itemGroup and one to remove an item from and itemGroup but I would like to add in some logic along the lines of it there is only 1 item left in the group removing that item should also remove the itemGroup.
here is an example of the relevant parts of my view model and view.
my JS
var ItemModel = function(item) {
var self = this;
self.name = ko.observable(item.name);
self.price = ko.observable(item.price);
};
var ItemGroupModel = function(itemGroup) {
var self = this;
self.order = ko.observable(itemGroup.order);
self.items = ko.observableArray(ko.utils.arrayMap(itemGroup.items, function(item){
return new ItemModel(item);
}));
self.type = ko.observable(item.type);
self.removeItem = function(item) {
self.items.remove(item);
}
};
var ViewModel = function(data) {
var self = this;
self.itemGroups = ko.observableArray(ko.utils.arrayMap(data.itemGroups, function(itemGroup) {
return new ItemGroupModel(item);
}));
// some other properties and methods
self.removeItemGroup = function(itemGroup) {
self.itemGroups.remove(itemGroup);
}
};
My View
<ul data-bind="foreach: {data: VM.itemGroups, as: 'itemGroup'}">
<li>
<button data-bind="click: $root.VM.removeItemGroup">X</button>
<ul data-bind="foreach: {data: itemGroup.items, as: 'item'}">
<li>
<!-- ko if: itemGroup.items().length > 1 -->
<button data-bind="click: itemGroup.removeItem">X</button>
<!-- /ko -->
<!-- ko ifnot: itemGroup.items().length > 1 -->
<button data-bind="click: function () { $root.VM.removeItemGroup($parent) }">X</button>
<!-- /ko -->
</li>
</ul>
</li>
</ul>
This works but to me it isnt ideal. It is my understanding that knockout should help me get away from using an anonymous function like "function () { $root.VM.removeItemGroup($parent) }" but I am not sure how to do it another way. Also removing the if and ifnot statements would be good to clean up as well.
I would like to give my solution
send index of itemGroups and items as argument to remove method.
Hope you know how to send index
Then check the length of itemGroups
self.remove(itemGroupsIndex,itemsIndex) {
var itemGroupsLength = self.itemGroups()[itemGroupsIndex].items().length;
if(itemGroupsLength = 1) {
self.itemGroups.remove(itemGroupsIndex);
}
else {
self.itemGroups()[itemGroupsIndex].items.remove(itemsIndex);
}
};