KNOCKOUT JS observableArray duplicate - javascript

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.

Related

Losing context in callback when invoked the second time backbone

The following code works, but I think there's room for improvement. The index check is there because after the first element is removed the next element looks like it has an index of -1, but is actually the previously removed element. Then it iterates again and finds the clicked element and removes it. BUT since the index is -1 on the first go around the wrong group gets deleted.
How do I keep the zombie elements from being iterated on more efficiently? This is in a backbone view with an in page confirmation.Thanks.
EDIT: To add HTML
Group section always has a default group that shouldn't be deleted.
<div class="section-border-top--grey js-favorite-group">
<h4 class="expandable__cta cta--std-teal js-expand-trigger"><span class="icon icon-plus--teal expandable__cta-icon"></span>All Doctors</h4>
<div class="expandable__content js-favorite-doctor-row-container" aria-expanded="true">
<div class="location-section dr-profile">
<div class="section__content js-doctor-row">
<div class="favorite-doctor-manage__row">
DR info
</div>
</div><!--/section__content-->
</div><!--/location-section-->
</div><!--/expandable__content-->
Tag section to remove groups
<div class="js-favorite-doctor-manage-add-remove">
<div class="grid-construct">
<div class="expandable" data-controller="expandable">
<ul class="tag-list js-group-list" tabindex="-1">
<li class="tag tag--teal" >
Lauren's Doctors
<ul class="tag-sub">
<li><button class="tag-icon tag-icon--close-white js-group-remove">Remove group: Lauren's Doctors</button></li>
</ul>
</li>
<li class="tag tag--teal" >
Timmy's Doctors
<ul class="tag-sub">
<li><button class="tag-icon tag-icon--close-white js-group-remove">Remove group: Timmy's Doctors</button></li>
</ul>
</li>
</ul>
</div>
removeGroup: function( evt ) {
var deleteGroup = function() {
if ( $(evt.currentTarget).closest('.tag').hasClass('is-active')){
var clickedTag = $(evt.currentTarget).closest('.tag');
var groupList = this.$el.find('.js-group-list');
var groupTags = groupList.find('.tag');
var index = groupTags.index(clickedTag);
var groupSections = $('.js-favorite-group');
// add one to account for "All" section which is never removed
var groupToRemove = groupSections.eq(index + 1);
console.log(groupToRemove);
var removedGroupName = this.getGroupNameForSection(groupToRemove);
var allDoctors = groupSections.eq(0);
var allDoctorsContainer = allDoctors.find('.js-favorite-doctor-row-container');
if ( index > -1 ){
groupToRemove.find('.js-favorite-doctor-row').appendTo(allDoctorsContainer);
clickedTag.remove();
groupToRemove.remove();
this.updateSectionDropdowns();
this.ariaAlert('Group ' + removedGroupName + ' removed');
this.hideConfirm(evt);
}
}
};
this.showAlert(evt, deleteGroup);
},
showAlert: function (evt, callback) {
that = this;
var clickedTag = '';
clickedTag = $(evt.currentTarget).closest('.tag');
clickedTag.addClass('is-active').attr('data-delete','true');
$('.delete-acct-message').show().focus();
$('.js-remove-yes').on('click', function(evt){
evt.preventDefault();
callback.apply(that);
});
$('.js-remove-no').on('click', function(evt){
evt.preventDefault();
this.hideConfirm(evt);
});
},
I would suggest that you should use custom attributes in your html, this will simplify your javascript logic and make it more effective and efficient.
I have modified your html and javascript to add the support for custom attribute data-doc-group. Have a look at your group sections div here
<div data-doc-group="lauren" class="section-border-top--grey js-favorite-group">
<h4 class="expandable__cta cta--std-teal js-expand-trigger"><span class="icon icon-plus--teal expandable__cta-icon"></span>Lauren's Doctors</h4>
<div class="expandable__content js-favorite-doctor-row-container" aria-expanded="true">
<div class="location-section dr-profile">
<div class="section__content js-doctor-row">
<div class="favorite-doctor-manage__row">
DR info
</div>
</div><!--/section__content-->
</div><!--/location-section-->
</div>
Here are the tags with custom attributes
<li data-doc-group="lauren" class="tag tag--teal">
Lauren's Doctors
<ul class="tag-sub">
<li><button class="tag-icon tag-icon--close-white js-group-remove">Remove group: Lauren's Doctors</button></li>
</ul>
</li>
<li data-doc-group="timmy" class="tag tag--teal">
Timmy's Doctors
<ul class="tag-sub">
<li><button class="tag-icon tag-icon--close-white js-group-remove">Remove group: Timmy's Doctors</button></li>
</ul>
</li>
Here is the javascript to handle this, (this may be a bit buggy, but will give you a general idea)
removeGroup: function(evt) {
this.showAlert(evt, function() {
var $clickedTag = $(evt.currentTarget).closest('.tag'),
dataGroupName,
$groupToRemove,
removedGroupName,
$allDoctors = $('.js-favorite-group').eq(0),
$allDoctorsContainer = $allDoctors.find('.js-favorite-doctor-row-container');
if ($clickedTag.hasClass('is-active')){
dataGroupName = $clickedTag.data('doc-group');
$groupToRemove = $allDoctors.siblings('[data-doc-group="' + docGroupName + '"]');
if ($groupToRemove.length > 0){
$groupToRemove.find('.js-favorite-doctor-row').appendTo($allDoctorsContainer);
$clickedTag.remove();
$groupToRemove.remove();
removedGroupName = this.getGroupNameForSection($groupToRemove);
this.updateSectionDropdowns();
this.ariaAlert('Group ' + removedGroupName + ' removed');
this.hideConfirm(evt);
}
}
});
}

Knockout.js: move checked items to top of list

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>

How to properly apply KO binding the following example

Basicaly, I have viewModel in KO having array of 2 values, I need to change css class prop of element (main) when <a> elemets are being clicked (when 1st li>a "Val1" clicked, <main class="stl1">... and so on). Strangely , nothing happends to <main>:
<script>
var mainViewModel = function () {
var self = this;
self.classArr = ['stl1', 'stl2'];
self.cssUsed = ko.observable(0);
self.getClass = function ( data, event ) {
var dat = event.target.value;
self.cssUsed = self.classArr[dat];
console.log( dat + ' : ' + self.cssUsed );
}
}
ko.applyBindings( new mainViewModel() );
</script>
<div class='page'>
<header>
<nav>
<ul >
<li>Val1</li>
<li>Val2</li>
</ul>
</nav>
</header>
<div id='maincontent'>
<main data-bind="css: cssUsed" >
<div class="center"></div>
</main>
</div>
</div>
You got it almost right. The problem was that your were assigning the value in the wrong way. Instead of
self.cssUsed = self.classArr[dat];
try
self.cssUsed(self.classArr[dat]);
Check it out here

removing a parent in a knockout function from nested loop

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);
}
};

Knockout refresh viewModel using mapping plugin

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.

Categories

Resources