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);
}
};
Related
I have the following code in html:
<ul data-bind="template: {name:'location', foreach:locations}">
</ul>
<script type="text/html" id="location">
<li>
<a href='#' id="search_results" data-bind='text: title' class='w3-bar-item'></a>
</li>
</script>
and the following code in viewModel:
var locations = [ (location lists)
];
var viewModel = {
title: ko.observable("Attractions in Seattle, Washington"),
query: ko.observable(""),
};
viewModel.locations = ko.dependentObservable(function(){
var search = this.query().toLowerCase();
return ko.utils.arrayFilter(locations, function(location) {
return location.title.toLowerCase().indexOf(search) >= 0;
});
}, viewModel);
ko.applyBindings(viewModel);
as shown below:
demo
and there is the following code in one of my regular javascript functions
$("#search_results").on('click', function() {
var context = ko.contextFor(this);
for (var i = 0; i < placeMarkers.length; i++) {
temp = placeMarkers[i].title + ", Seattle";
if (temp == context.$data.title) {
getPlacesDetails(placeMarkers[i], placeInfoWindow);
}
}
});
I am trying to dynamically show the result based on what context the user clicks, but my function works only for the first item in the list (only Space Needle, in this case). How can I fix it? what would be knockout.js-ic way?
+
I wrote like this inside of viewModel:
show_infowindow: function() {
var context = ko.contextFor(this);
for (var i = 0; i < placeMarkers.length; i++) {
temp = placeMarkers[i].title + ", Seattle";
if (temp == context.$data.title) {
getPlacesDetails(placeMarkers[i], placeInfoWindow);
}
}
}
where
<a href='#' data-bind='text: title, click: show_infowindow' class='search_results w3-bar-item'></a>
and now nothing is working, how can I fix this?
I suggest you create viewModel function and use the new operator whenever you have a click function or a computed property (or dependentObservable prior to ko 2.0). This will reduce the pain of debugging and understanding what this means in callbacks.
So remove the jquery click event handler and change your viewmodel to:
var viewModel = function() {
var self = this;
self.title = ko.observable("Attractions in Seattle, Washington");
self.query = ko.observable("");
self.locations = ko.computed(function(){
var search = self.query().toLowerCase();
return ko.utils.arrayFilter(locations, function(location) {
return location.title.toLowerCase().indexOf(search) >= 0;
});
}
self.show_infowindow = function(location){
// "location" parameter has the current location object being clicked
// you can use it directly instead of ko.contextFor(this);
}
};
// don't forget the "new" keyword
ko.applyBindings(new viewModel());
Change your template to add a click binding like this:
<script type="text/html" id="location">
<li>
<a href='#' id="search_results" data-bind='text: title, click:$parent.show_infowindow' class='w3-bar-item'></a>
</li>
</script>
Since you are using the click binding inside a foreach, you need to prefix the click function with $parent keyword to get the proper binding context. Without $parent, knockout will look for show_infowindow in each location object instead of your viewModel.
Here's another useful answer on the differences between viewModel as an object literal vs a function
I'm using knockout.js (v. 3.2.0) to create a series of checkbox lists on a page. For any given list I want checked items to move to the top of the list, and move below the checked items when unchecked.
At first I thought I could use a js function to reorder the list and then call the function after ko.applyBindings which is apparently synchronous (calling it before applyBindingds wouldn't work because the DOM isn't complete yet). Nevertheless, it didn't work. Any idea how to write a function in the view model that will do this?
Here is my markup:
<ul data-bind="foreach: targetingViewModel.filteredTargetingInstances('assets')">
<li>
<label>
<input type="checkbox" data-bind="checked:selected"/>
<span data-bind="text: value"></span>
</label>
</li>
</ul>
And here is the jQuery function I found in another Stack Overflow answer:
addSelectionsToList = function () {
var $list = $(".target-list");
sortItems($list);
function sortItems(list){
var origOrder = list.children();
list.on("click", ":checkbox", function() {
var i,
checked = document.createDocumentFragment(),
unchecked = document.createDocumentFragment();
for (i = 0; i < origOrder.length; i++) {
if (origOrder[i].getElementsByTagName("input")[0].checked) {
checked.appendChild(origOrder[i]);
} else {
unchecked.appendChild(origOrder[i]);
}
}
list.append(checked).append(unchecked);
});
}
};
Thanks.
Check out following Code snippet. hope this is will help you.
Steps I have performed
Created one viewmodel with class Item with subscribe on change of
isChecked property.
There is Obsservable array of list items which
will contain isChecked property.
Finally on change of value it
will call the sorting by selecting value on change in any item.
function SomeViewModel() {
var self = this;
self.chkLists = ko.observableArray([]);
function Item(isChecked, name) {
this.isChecked = ko.observable(isChecked);
this.name = ko.observable(name);
this.isChecked.subscribe(function (newValue) {
self.chkLists.sort(function (l) { return l.isChecked() === false });
})
}
self.chkLists = ko.observableArray([
new Item(false, "Bear"),
new Item(false, "Hippo"),
new Item(false, "Lion"),
new Item(false, "Tiger"),
new Item ( false, "Zebra" )
]);
}
$(document).ready(function () {
var divEle = $("#sortedList")[0];
ko.applyBindings(SomeViewModel, divEle);
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div id="sortedList">
<ul data-bind="foreach: chkLists">
<li>
<label>
<input type="checkbox" data-bind="checked: $data.isChecked" />
<span data-bind="text: $data.name"></span>
</label>
</li>
</ul>
</div>
I am new to knockout. For my problem, I am trying to make it so that for each project, there is a button and textarea. The textarea will be hidden upon page load. If I click the button, it will show the textarea (toggle). Currently, if I click the button, ALL textareas on the page will show, rather than just the corresponding textarea.
I'm hoping the fix for this isn't too dramatic and involving a complete reworking of my code as by some magic, every other functionality has been working thus far. I added the {attr id: guid} (guid is a unique identifier of a project retrieved from the database) statement in an attempt to establish a unique ID so that the right controls were triggered...although that did not work.
Sorry I do not have a working jfiddle to show the issue... I tried to create one but it does not demonstrate the issue.
JS:
//if a cookie exists, extract the data and bind the page with cookie data
if (getCookie('filterCookie')) {
filterCookie = getCookie('filterCookie');
var cookieArray = filterCookie.split(",");
console.log(cookieArray);
$(function () {
var checkboxes = new Array();
for (var i = 0; i < cookieArray.length; i++) {
console.log(i + cookieArray[i]);
checkboxes.push(getCheckboxByValue(cookieArray[i]));
//checkboxes.push(document.querySelectorAll('input[value="' + cookieArray[i] + '"]'));
console.log(checkboxes);
checkboxes[i].checked = true;
}
})
filterCookie = getCookie('filterResultsCookie');
cookieArray = filterCookie.split(",");
filterCookieObj = {};
filterCookieObj.action = "updateProjects";
filterCookieObj.list = cookieArray;
$.ajax("/api/project/", {
type: "POST",
data: JSON.stringify(filterCookieObj)
}).done(function (response) {
proj = response;
ko.cleanNode(c2[0]);
c2.html(original);
ko.applyBindings(new ProjectViewModel(proj), c2[0]);
});
}
//if the cookie doesn't exist, just bind the page
else {
$.ajax("/api/project/", {
type: "POST",
data: JSON.stringify({
action: "getProjects"
})
}).done(function (response) {
proj = response;
ko.cleanNode(c2[0]);
c2.html(original);
ko.applyBindings(new ProjectViewModel(proj), c2[0]);
});
}
View Model:
function ProjectViewModel(proj) {
//console.log(proj);
var self = this;
self.projects = ko.observableArray(proj);
self.show = ko.observable(false);
self.toggleTextArea = function () {
self.show(!self.show());
};
};
HTML:
<!-- ko foreach: projects -->
<div id="eachOppyProject" style="border-bottom: 1px solid #eee;">
<table>
<tbody>
<tr>
<td><a data-bind="attr: { href: '/tools/oppy/' + guid }" style="font-size: 25px;"><span class="link" data-bind=" value: guid, text: name"></span></a></td>
</tr>
<tr data-bind="text: projectDescription"></tr>
<%-- <tr data-bind="text: guid"></tr>--%>
</tbody>
</table>
<span class="forminputtitle">Have you done project this before?</span> <input type="button" value="Yes" data-bind="click: $parent.toggleTextArea" class="btnOppy"/>
<textarea placeholder="Tell us a little of what you've done." data-bind="visible: $parent.show, attr: {'id': guid }" class="form-control newSessionAnalyst" style="height:75px; " /><br />
<span> <input type="checkbox" name="oppyDoProjectAgain" style="padding-top:10px; padding-right:20px;">I'm thinking about doing this again. </span>
<br />
</div><br />
<!-- /ko -->
Spencer:
function ProjectViewModel(proj) {
//console.log(proj);
var self = this;
self.projects = ko.observableArray(proj);
self.projects().forEach(function() { //also tried proj.forEach(function())
self.projects().showComments = ko.observable(false);
self.projects().toggleComments = function () {
self.showComments(!self.showComments());
};
})
};
It's weird that
data-bind="visible: show"
doesn't provide any binding error because context of binding inside ko foreach: project is project not the ProjectViewModel.
Anyway, this solution should solve your problem:
function ViewModel() {
var self = this;
var wrappedProjects = proj.map(function(p) {
return new Project(p);
});
self.projects = ko.observableArray(wrappedProjects);
}
function Project(proj) {
var self = proj;
self.show = ko.observable(false);
self.toggleTextArea = function () {
self.show(!self.show());
}
return self;
}
The problem is that the show observable needs to be defined in the projects array. Currently all the textareas are looking at the same observable. This means you'll have to move the function showTextArea into the projects array as well.
Also you may want to consider renaming your function or getting rid of it entirely. Function names which imply they drive a change directly to the view fly in the face of the MVVM pattern. I'd recommend a name like "toggleComments" as it doesn't reference a view control.
EDIT:
As an example:
function ProjectViewModel(proj) {
//console.log(proj);
var self = this;
self.projects = ko.observableArray(proj);
foreach(var project in self.projects()) {
project.showComments = ko.observable(false);
project.toggleComments = function () {
self.showComments(!self.showComments());
};
}
};
There is probably a much cleaner way to implement this in your project I just wanted to demonstrate my meaning without making a ton of changes to the code you provided.
I am trying refresh a small widget with knockout and the mapping plugin. Here is my code so far:
var AppViewModel = function (data, total, qty) {
var self = this;
self.Products = ko.mapping.fromJS(data, {}, this);
self.CartTotals = ko.observable(total);
self.TotalQty = ko.observable(qty);
};
var func = function(u) {
return $.ajax({ type: "POST", contentType: "application/json; charset=utf-8", data: "{}", dataType: "json", url: u });
},
getQtyTotal = function(b) {
var a = 0;
$.each(b.Table.Rows, function(c) {
a += parseInt(b.Table.Rows[c].Quantity) || 0;
});
return a;
};
$.when(func("/store/MiniCart.aspx/GetShoppingCartInfo"), func("/store/MiniCart.aspx/GetCartTotal")).done(function (jsonA, jsonB) {
var ds = $.parseJSON(jsonA[0].d), ds2 = $.parseJSON(jsonB[0].d), qtyTotal = getQtyTotal(ds);
ko.applyBindings(new AppViewModel(ds, ds2, qtyTotal));
});
<div class="cartDropDownProductItemWrapper" data-bind="foreach: Products.Table.Rows">
<div class="cartDropDownProductItem">
<div class="cartDropDownProductImg">
<img id="cart_details_rpt_prod_image_0" style="height: 71px; width: 55px;" data-bind="attr: { src: ProductImageURL }">
</div>
<div class="cartDropDownProductDesc">
<h6><a data-bind="text: ModelName, attr: { href: ProductLink }"></a></h6>
<div class="cartDropDownProductDescInner">
<div class="cartDropDownColor"> COLOR
<strong><span data-bind="text:ColorName"></span></strong>
</div>
<div class="cartDropDownSize"> SIZE
<strong><span data-bind="text: SizeName"></span></strong>
</div>
<div class="cartDropDownSize"> QTY
<strong><span data-bind="text: Quantity"></span></strong>
</div>
<div class="cartDropDownPrice"> PRICE
<strong><span data-bind="text: UnitCost().toFixed(2)"></span></strong>
</div>
<div class="cartDropDownRemove">
<a href="javascript:void(0);" class="remove" onclick="removeItem('v3BuhngpE4c=')">
<img src="/images/layout/icons/remove.gif" alt="Remove Item">
</a>
</div>
</div>
</div>
<div class="clear"></div>
</div>
<!-- end fo reach -->
<div class="clear"></div>
<div class="cartDropDownButtons clearfix">
<ul class="clearfix">
<li class="countItems"><span data-bind="text: TotalQty"></span> Items</li>
<li class="subTotal" id="subTotal">SUBTOTAL: $<span data-bind="text: CartTotals().toFixed(2)"></span></li>
</ul>
</div>
It renders fine intially but when I try to rebind on a jQuery click event and call:
ko.applyBindings(new AppViewModel(ds, ds2, qtyTotal));
It duplicates the data.
If you start off by creating an empty viewModel... which takes no arguments via it's constructor, like so:
function ViewModel()
{
var self = this;
}
var viewModel = new ViewModel();
...Then you can reference it by name to load in your data using ko.mapping, like this:
ko.mapping.fromJS({ "PropertyName": plainJsObject }, {}, viewModel);
What this does is runs the ko.mapping magic on plainJsObject, and stuffs the result into a property (in this case, called PropertyName) on your viewModel object.
The part you would particularly care about is this:
If you want to refresh the data located in the viewModel.PropertyName with fresh data from the server... you just call the exact same method, and it updates that same property on your viewModel. Just call this same thing again, with new values in your plainJsObject:
ko.mapping.fromJS({ "PropertyName": plainJsObject }, {}, viewModel);
Since you have (I assume) already performed your ko.applyBindings() at some point in your code, this line above will immediately update your view.
You should only need to perform the binding once so just keep that outside of any event driven function calls.
ko.applyBindings(new AppViewModel(ds, ds2, qtyTotal));
I think you are over complicating your code in the above example. I would just return set of Product objects which contains properties for description, unit price, quantity etc and then to calculate the total, use a ko computed variable which will update automatically if a user tries to increase/decrease quantity.
function Product(item) {
var self = this;
self.description = ko.observable(item.description);
self.quantity = ko.observable(item.quantity);
self.unitPrice = ko.observable(item.unitPrice);
self.total = ko.computed(function() {
return self.quantity() * self.unitPrice();
});
}
function AppViewModel() {
var self = this;
self.products = ko.observableArray([]);
self.overvallTotal = ko.computed(function() {
var total = 0;
for (var i = 0; i < self.products().length; i++) {
total += self.products()[i].total;
}
return total;
});
self.removeProduct = function(item) { self.products.remove(item) };
// Load initial state from server, convert it to Product instances
$.getJSON("/store/MiniCart.aspx/GetShoppingCartInfo", function(allData) {
var mappedProducts = $.map(allData, function(item) { return new Product(item) });
self.products(mappedProducts);
});
}
ko.applyBindings(new AppViewModel());
You will have to adjust your server side json results to work with this.
I have an array that I'm removing items from but I'm keeping count of the number of items to do UI formatting. I need to be able to have the bind update.
ko.applyBindings(viewModel);
getFoos();
var viewModel = {
foos: ko.observableArray([]),
reloadFoos: function () {
getFoos();
},
removeFoo: function () {
remove(this);
}
};
var foo = function () {
this.Id = ko.observable();
this.Name = ko.observable();
this.Count = ko.observable();
};
function remove(foo) {
viewModel.foos.splice(viewModel.foos.indexOf(foo), 1);
viewModel.foos.each(function(index) {
viewModel.foos[index].Count = index%10 == 0;
});
}
function getFoos() {
viewModel.foos([]);
$.get("/myroute/", "", function (data) {
for (var i = 0; i < data.length; i++) {
var f = new foo();
f.Id = data[i];
f.Name = data[i];
f.Count = i%10 == 0;
viewModel.foos.push(f);
}
});
}
<div data-bind="foreach: foos">
<div style="float: left">
<a href="javascript:void(0);" data-bind="click : $parent.removeFoo, attr: { id: Id }">
<label data-bind="value: Name"></label>
</a>
</div>
<!-- ko if: Count -->
<div style="clear: left"></div>
<!-- /ko -->
</div>
When the click event fires the item is removed from the array but the if bind doesn't get updated and the ui formatting is off. I'm trying to keep from reloading the data because the ui block bounces as it removes and reloads.
Your UI is not being updated because when you do your assignment to Count, you aren't assigning as an observable. You are replacing the observable with a straight boolean value. So, your assignment calls like this one:
viewModel.foos[index].Count = index%10 == 0;
Will cause viewModel.foos[index].Count to be equal to true or false and the value won't be stored in the observable.
That line should be this instead:
viewModel.foos[index].Count(index%10 == 0);
That will set the observable correctly. Note that you must change all of your assignments to observables to be set this way. See the "Reading and Writing Observables" section of this page: Knockout Observables.