Knockout doesn't update when the array changes? - javascript

I have a Knockout UI where I am putting objects into observables. Once you click an add to cart button that updates the object, the ui doesn't update. Once you refresh the page it will update.
I have heard to a lot of people having this TYPE of issue, but I haven't seen anything pertaining to this case.
Global Vars:
var koCart, koQuantity, koTotal;
Function:
function updateCart() {
**parse data **
//assign data to observables
koCart = ko.observableArray(domecart)
koQuantity = ko.observable(quantity)
koTotal = ko.observable(total)
}
View Model:
function viewModel() {
self = this;
this.newcart = koCart();
this.total = koTotal();
this.quantity = koQuantity();
}
var element = document.getElementById('cart');
var element2 = $('.floatingTab')[0];
var app = new viewModel();
ko.applyBindings(app, element);
ko.applyBindings(app, element2);
Cart with KO Bindings
<nav class="cbp-spmenu cbp-spmenu-vertical cbp-spmenu-right shopify-buy__cart" id="cbp-spmenu-s2">
<div class="shopify-buy__cart__header">
<h2 class="shopify-buy__cart__title">Cart</h2>
<button class="shopify-buy__btn--close">
<span aria-role="hidden" id="x-menu">×</span>
</button>
</div>
<div id="cart" class="shopify-buy__cart-scroll">
<div class="shopify-buy__cart-items" data-bind="foreach: newcart">
<div class="shopify-buy__cart-item">
<div data-bind="style: { 'background-image': 'url(' + images + ')'}" class="shopify-buy__cart-item__image" alt="Product" style="background-repeat:no-repeat;background-size: contain;"></div>
<span class="shopify-buy__cart-item__title" data-bind="text: name"></span>
<span class="shopify-buy__cart-item__price" data-bind="text: price "></span>
<div class="shopify-buy__quantity-container">
<button class="shopify-buy__btn--seamless shopify-buy__quantity-decrement" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M4 7h8v2H4z"></path></svg>
</button>
<input class="shopify-buy__quantity shopify-buy__cart-item__quantity-input" type="number" min="0" aria-label="Quantity" data-bind="attr: {value: quantity}" style="height: 30px; border:solid 1px #d3dbe2 !important;padding-left:13px;" />
<button class="shopify-buy__btn--seamless shopify-buy__quantity-increment" type="button" databind="click: addToCard(id)" >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M12 7H9V4H7v3H4v2h3v3h2V9h3z"></path></svg>
</button>
</div>
</div>
</div>
<div class="shopify-buy__cart-bottom">
<p class="shopify-buy__cart__subtotal__text" >SUBTOTAL</p>
<p class="shopify-buy__cart__subtotal__price"data-bind="text: total"></p>
<p class="shopify-buy__cart__notice">Shipping and discount codes are added at checkout.</p>
<button class="shopify-buy__btn shopify-buy__btn--cart-checkout" type="button">CHECKOUT</button>
</div>
</div>
</nav>
Also, the error being thrown is, koCart is not a function.

I would avoid using the global variables like that and include everything in a properly constructed viewmodel. You might do something like this where you first define your observables and then populate them from within your viewmodel:
function viewModel() {
var self = this;
self.newcart = ko.observableArray();
self.total = ko.observable();
self.quantity = ko.observable();
self.updateCart = function() {
**parse data **
//assign data to observables
self.newcart(domecart)
self.quantity(quantity)
self.total(total)
}
}
var element = document.getElementById('cart');
var element2 = $('.floatingTab')[0];
var app = new viewModel();
ko.applyBindings(app, element);
ko.applyBindings(app, element2);
This does depend on how you get your data and when you need to call the updateCart function. Doing this could prove benefical as the update function could be in a computed that will automatically update through the user interaction in the UI.

Related

How to access knockout $index in javascript

I need to get the $index in a javascript function to implement prev/next buttons for a list of values. The method I found seems cumbersome, perhaps there is a more straightforward way.
Now I do this to get the $index in javascript, then put it in an observable:
<div data-bind="foreach: myarray">
<div data-bind="click: function(data, event){ onclick(data, event, $index()); }"
function onclick(idata, event, index) {
theAppViewModel.choiceindex(index);
On SO I found a small improvement by getting $index from the event:
<div data-bind="foreach: myarray">
<div data-bind="click: onclick"
function onclick(idata, event) {
var context = ko.contextFor(event.target);
theAppViewModel.choiceindex(context.$index());
From a commenter came the method to get the index by searching the array for the selected value, that usually has its own observable, e.g. choice:, like:
var i = TheArray.indexOf(theAppViewModel.choice());
Normally, the length of the array on the page isn't huge, and if it has big objects, you could just search over one of its fields with fun syntax like:
myarray.find(x => x.id === searchvalue);
But I wonder if it isn't possible to access the $index even more directly, without storing in my own observable choiceindex, as the Knockout docs say that $index is already an observable.
Here is a complete sample code to play with:
<!DOCTYPE html>
<html>
<head>
<title>Test Knockout Foreach</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src='https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.1/knockout-min.js'></script>
<style>
.selected {background-color: #f0f0f0;}
</style>
</head>
<body>
<div data-bind="foreach: myarray" style="width: 10em;">
<div data-bind="click: function(data, event){ onclick(data, event, $index()); },
css: { selected: $data == $root.choice() }">
<span data-bind="text: $index"></span>
<span data-bind="text: $data"></span>
</div>
</div>
<input type="button" value="Prev" data-bind="click: onprev" />
<input type="button" value="Next" data-bind="click: onnext" />
<p>
Choice:
<span data-bind="text: choice"></span>
</p>
<p>
Index:
<span data-bind="text: choiceindex"></span>
</p>
<script>
var TheArray = ["apple", "pear", "banana", "coconut", "peanut"];
function onclick(idata, event, index) {
theAppViewModel.choice(idata);
theAppViewModel.choiceindex(index);
//var context = ko.contextFor(event.target);
//theAppViewModel.choiceindex(context.$index());
//theAppViewModel.choiceindex(index);
}
function onprev(idata, event) {
var i = theAppViewModel.choiceindex() - 1;
if (i >= 0) {
theAppViewModel.choice(TheArray[i]);
theAppViewModel.choiceindex(i);
}
}
function onnext(idata, event) {
//var i = theAppViewModel.choiceindex() + 1;
//var dummydata = theAppViewModel.choice();
//var dummy = TheArray.indexOf(dummydata);
var i = TheArray.indexOf(theAppViewModel.choice()) + 1;
if (i < TheArray.length) {
theAppViewModel.choice(TheArray[i]);
theAppViewModel.choiceindex(i);
}
}
function AppViewModel() {
var self = this;
self.myarray = ko.observableArray(TheArray);
self.choice = ko.observable();
self.choiceindex = ko.observable();
}
var theAppViewModel = new AppViewModel();
window.onload = function () {
ko.applyBindings(theAppViewModel);
}
</script>
</body>
</html>
There is no built-in binding to set a viewmodel value from a binding, but it's simple create one to do so:
ko.bindingHandlers.copyIndex = {
init: function (element, valueAccessor, allBindings, vm, bindingContext) {
vm.index = bindingContext.index;
}
};
The use it as follows:
<div data-bind="foreach: myarray">
<div data-bind="copyIndex"></div>
</div>
However, I'd still not recommend this approach, since it ties viewmodel behavior to the presence of specific bindings. Jason Spake's suggestion to use myarray.indexOf($data) (or ko.utils.arrayIndexOf(myarray, $data)) would be more robust.
write 'index' and not 'index()' in the html part.
Try this:
<div data-bind="foreach: myarray" style="width: 10em;">
<div data-bind="click: function(data, event){ onclick(data, event, $index},
css: { selected: $data == $root.choice() }">
<span data-bind="text: $index"></span>
<span data-bind="text: $data"></span>
</div>
</div>

How update list in AngularJS?

I created one TODO List in AngularJS but I didn't finish the update function. I have some difficulty.
How do I create the update function?
Link: https://plnkr.co/edit/XfWoGVrEBqSl6as0JatS?p=preview
<ul class="list-todo">
<li ng-repeat="t in tasks track by $index">
<div class="row">
<div class="six columns">
<p>{{ t }}</p>
</div>
<div class="three columns">
<button class="button" ng-click="update()">update</button>
</div>
<div class="three columns">
<button class="button" ng-click="delete()">x</button>
</div>
</div>
</li>
</ul>
Angular Code:
angular.module('todoTest', [])
.controller('todoController', function($scope) {
$scope.tasks = [];
$scope.add = function() {
$scope.tasks.push($scope.dotask);
}
$scope.update = function(){
$apply.tasks.push($scope.dotask);
}
$scope.delete = function() {
$scope.tasks.splice(this.$index, 1);
}
})
If you want to update the value you have to pass as parameter the position of the task inside the tasks array ($index is the position):
<button class="button" ng-click="update($index)">update</button>
And then the update function would be:
$scope.update = function(index){
$scope.tasks[index] = $scope.dotask;
}
Is that what you needed?
Button for updating. Visible only while updating.
<div class="row">
<button type="submit" class="u-full-width button button-primary">Criar Tarefa</button>
<button ng-show="updateMode" ng-click="update()" type="button" class="u-full-width button button-primary">Update</button>
</div>
New update function.
$scope.update = function(index) {
if ($scope.updateMode) {
$scope.tasks[$scope.updateIndex] = $scope.dotask;
$scope.updateMode = false;
} else {
$scope.dotask = $scope.tasks[index];
$scope.updateMode = true;
$scope.updateIndex = index;
}
If you click update on the task it will make the big update button visible and bring the old todo to the input. After hitting the big update button the todo task will update.
Plunker

knockout js prevents existing html observable from updating

I'm just starting learning knockout.js and am a bit stuck on managing observables and updating them correctly. See the jsfiddle for the code.
self.addItem = function(item, event){
ptitle = $(event.currentTarget).prev('h3').text();
self.items.push({
productQty: self.productQty(),
productClip: self.productClip(),
productTitle: ptitle
});
}
http://jsfiddle.net/L61qhrc1/
What I have is a list of existing html elements. I want to create another list from that list with some input fields that can be set. It's mostly working but I cannot figure out from the examples around the net I've been looking at.
when one field updates all the fields in the list update but I only want the field I'm currently updating to be updated not the whole list.
Can any kind person point me in the right direction?
Cheers.
As I said in my comment, your user interface has to be a logical consequence of your data and viewmodel. Your viewmodel must never be concerned with the details of the view.
Also, your fiddle looks pretty over-engineered to me.
The following is what I have gathered from your sample, but in a less byzantine way.
Make separate self-contained viewmodels. Ideally make them so that they can bootstrap themselves from the data you pass to the constructor.
Working with templates keeps the HTML of the view clean and improves modularity and reusability.
For more complex data, consider the the mapping plugin to bootstrap your models.
Consult Unique ids in knockout.js templates for a way to create working <input> / <label> pairs.
function ListItem(data, parent) {
var self = this;
data = data || {};
self.productQty = ko.observable(data.productQty);
self.productClip = ko.observable(!!data.productClip);
}
function Product(data) {
var self = this;
data = data || {};
self.title = ko.observable(data.title);
self.id = ko.observable(data.id);
self.items = ko.observableArray();
self.newItem = new ListItem();
self.addItem = function () {
self.items.push(new ListItem(ko.toJS(self.newItem), self));
};
self.removeItem = function (item) {
self.items.remove(item);
};
}
function ProductList(data) {
var self = this;
data = data || {};
self.products = ko.observableArray(ko.utils.arrayMap(data.products, function (p) {
return new Product(p);
}));
}
var vm = new ProductList({
products: [
{title: "ProductName 1", id: "ProductId 1"},
{title: "ProductName 2", id: "ProductId 2"}
]
});
ko.applyBindings(vm);
ul {
list-style-type: none;
padding: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div class="container">
<div class="m-col-6">
<ul data-bind="foreach: products">
<li data-bind="template: 'productTemplate'"></li>
</ul>
</div>
</div>
<hr />
<pre data-bind="text: ko.toJSON($data, null, 2)"></pre>
<script type="text/html" id="productTemplate">
<h1 data-bind="text: title"></h1>
<p data-bind="text: id"></p>
<div data-bind="with: newItem">
<input type="checkbox" data-bind="checked: productClip" />
<label>has pump clips</label>
<input type="number" data-bind="value: productQty" />
<button data-bind="click: $parent.addItem">add to list</button>
</div>
<ul data-bind="foreach: items">
<li>
<!-- ko template: 'itemsTemplate' --><!-- /ko -->
<button data-bind="click: $parent.removeItem">Remove</button>
</li>
</ul>
</script>
<script type="text/html" id="itemsTemplate">
<b data-bind="text: $parent.title"></b>
<span data-bind="text: productClip() ? 'has' : 'does not have'"></span> pump clips
(<span data-bind="text: productQty"></span>)
</script>

Repeat div every click angular

I have a counter in angularjs
$scope.count = 0;
$scope.increment = function(){
$scope.count = $scope.count + 1;
console.log("count " + $scope.count);
};
$scope.decrement = function() {
if($scope.count > 0) {
$scope.count = $scope.count - 1;
}
};
and i would display a div if the count value is more than 1. The counter is incremented by a click in a button. Every time i click i increment the counter. Actually the counter goes well but i would create a div in each click. Now i'm be able to generate only one div. This is html part
<a class="uk-button" data-ng-click="increment()">
<i class="uk-icon-plus" ></i>
New div
</a>
<div class="uk-panel" data-ng-if="count >= 1">
<hr>
<div class="uk-panel uk-width-1-1 qt-add-row">
<form class="uk-form uk-form-horizontal uk-float-left">
<div class="uk-form-row" data-ng-repeat="item in data.row">
<label class="uk-form-label" for="form">{{item.name}}</label>
<div class="uk-form-controls uk-width-1-1">
<input id="form" class="uk-width-1-1" type="text" placeholder="{{item.name}}" ng-model="field.value">
<span style="position: absolute" class="uk-button-group">
<a class="uk-button" href="#">
<i class="uk-icon-close"></i>
</a>
</span>
</div>
</div>
</form>
</div>
</div>
You can add ng-repeat directive to your div. Unfortunately ng-repeat accepts only objects and arrays, not numbers. So you'll have to also modify your increment/decrement functions a bit
<div class="uk-panel" data-ng-if="countArr.length > 0" data-ng-repeat="i in countArr">
$scope.countArr = [];
$scope.increment = function() {
$scope.countArr.push('');
};
$scope.decrement = function() {
$scope.countArr.splice(0, 1);
};

AngularJS not filtering products list

I am having an issue with AngularJS filtering my products list. I have done a few console.log queries on my variables and all seem fine. The problem is that the view does not update to show the filtered products.
Filtering works perfectly fine when you enter the search text in the input box but it does not work when clicking on the Category menu items.
I would like to filter the product list by category when the user clicks on the menu item.
Please see my code below and any help or advice is greatly appreciated.
My app.js
myApp.controller('StoreController', function($scope, $filter, storeFactory, cartFactory) {
$scope.cartTotal = 0;
$scope.cartItems = [];
$scope.categories = [];
$scope.counted = 0;
$scope.filteredProducts = {};
//get the products
storeFactory.getProducts(function(results) {
$scope.products = results.products;
$scope.counted = $scope.products.length;
$scope.filteredProducts = results.products;
});
$scope.$watch("search", function(query){
if($scope.filteredProducts.length) {
$scope.filteredProducts = $filter("filter")($scope.products, query);
$scope.counted = $scope.filteredProducts.length;
}
});
$scope.filterProductsByCategory = function(categoryName){
console.log('category filter');
/*$scope.products.forEach(function(o,i){
console.log('filter');
if( o.category_id !== categoryId ){
$scope.filteredProducts.splice(i,1);
console.log('removing');
console.log(o);
}
});*/
$scope.filteredProducts = $filter("filter")($scope.filteredProducts, categoryName);
console.info('the filtered items');
console.log($scope.filteredProducts);
$scope.counted = $scope.filteredProducts.length;
}
$scope.getCategories = function(){
storeFactory.getCategories(function(results){
$scope.categories = results.rows;
});
}
$scope.getCategories();
});
My store.htm
UPDATE :
I removed the extra controller reference and wrapped everything in one div.
<div ng-controller="StoreController">
<!-- the sidebar product menu -->
<div class="block-content collapse in">
<div class="daily" ng-repeat="category in categories | orderBy:'name'">
<div class="accordion-group">
<div ng-click="filterProductsByCategory(category.category_name)" class="accordion-toggle collapsed">{{category.category_name}}</div>
</div>
</div>
</div>
</div>
<!-- Load store items start -->
<div class="label">Showing {{counted}} Product(s)</div>
<div class="row" ng-repeat="product in filteredProducts | orderBy:'product_name'" style="margin-left: 0px; width: 550px;">
<hr></hr>
<div class="span1" style="width: 120px;">
<!-- <a data-toggle="lightbox" href="#carouselLightBox"> -->
<a data-toggle="lightbox" href="#carouselLightBox{{product.product_id}}">
<!--img alt={{ product.product_name }} ng-src="{{product.image}}" src="{{product.image}}" /-->
<img id="tmp" class="" src="images/products/{{product.product_image_filename}}" alt=""></img>
</a>
<div class="lightbox hide fade" id="carouselLightBox{{product.product_id}}" style="display: none;">
<div class='lightbox-content'>
<img src="images/products/{{product.product_image_filename}}" alt="" />
<!--img alt={{ product.product_name }} ng-src="{{product.image}}" src="{{product.image}}" /-->
<button class="btn btn-primary" id="close_lightbox" ng-click="closeBox(product.product_id, $event)">Close</button>
</div>
<style>
#close_lightbox
{
position: absolute;
top: 5px;
right: 5px;
}
</style>
</div>
</div>
<div class="span6" style="width: 330px; margin-bottom: 15px;">
<h5 style="font-size: 14px; font-weight: bold;">{{product.product_name}}</h5>
<p>{{product.product_description }}</p>
<p>Category : {{product.category_name}}</p>
</div>
<div class="span3">
<p class="price">Price : <strong>R{{ product.product_price }}</strong></p>
</div>
</div>
<!-- end of controller -->
</div>
$scope.filteredProducts = {};
//get the products
storeFactory.getProducts(function (results) {
$scope.products = results.products;
$scope.counted = $scope.products.length;
$scope.filteredProducts = results.products;
});
Is results.products an array of objects or an object of objects? Because $filter('filter')($scope.products,query); expects $scope.products to be an array.
$scope.$watch("search", function (query) {
if($scope.filteredProducts.length) {
$scope.filteredProducts = $filter("filter")($scope.products, query);
$scope.counted = $scope.filteredProducts.length;
}
});
Here's what I'm thinking this should look like and you won't need the $watch statement or the filteredProducts array/object, In controller:
$scope.search = '';
$scope.products = [];
$scope.categories = ['category1','category2','category3'];
// assuming that your store function getProducts returns a promise here
storeFactory.getProducts().then(function(results){
$scope.products = results.products; // needs to be an array of product objects
});
$scope.filterProductsByCategory = function(category){
$scope.search = category;
};
Then in your HTML Partial, this is not exactly how your's is, I'm just showing you here in shorter form what is possible:
<button ng-repeat="cat in categories" ng-click="filterProductsByCategory(cat)">{{cat}}</button>
<div class="row" ng-repeat="product in products | filter:search | orderBy:'product_name'">
<!-- Product Information Here -->
</div>
I created a JSFiddle to demonstrate: http://jsfiddle.net/mikeeconroy/QL28C/1/
You can free form search with any term or click a button for a category.
The side bar is using one instance of StoreController, and the main div is using another instance of StoreController. Each instance having its own scope. The should both use the same controller instance: wrap everything inside a div, and use a unique StoreController for this wrapping div.

Categories