knockout js prevents existing html observable from updating - javascript

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>

Related

Knockout doesn't update when the array changes?

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.

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>

Array with dropdowns in Knockout JS

Good Afternoon everyone.
I've tried to search it, but either I'm searching for a wrong thing or there is no answer to that yet. I'm trying to make a list in knockout with a list of drop-downs, and when those drop-down values change, the corresponding value in the list needs to be updated. Here is my current "vision" of that, which doesn't work.
http://jsfiddle.net/Lypmnspz/6/
Here is the code from Fiddle:
function pageModel(){
var self = this
// Create an observalbe array of options
self.languages = ko.observableArray(["English","English","English"]);
// Languages
self.availableLanguages = ko.observableArray(["English", "Spanish", "German", "Russian"]);
}
ko.applyBindings(new pageModel());
<h4>When I update it here</h4>
<ul data-bind="foreach: languages">
<li><select data-bind="options: $parent.availableLanguages, value: $data"></select></li>
</ul>
<h4>I want to see the update here</h4>
<ul data-bind="foreach: languages">
<li data-bind="text: $data"></li>
</ul>
Can anyone suggest something? Thank you.
Maybe you need another data model - let's call it a LanguageSelection:
var LanguageSelection,
PageModel,
pageModel;
LanguageSelection = function LanguageSelection() {
this.language = ko.observable();
};
PageModel = function PageModel(){
this.availableLanguages = ko.observableArray([
"English",
"Spanish",
"German",
"Russian"
]);
this.languageSelections = ko.observableArray([]);
}
pageModel = new PageModel();
pageModel.languageSelections.push( new LanguageSelection() );
pageModel.languageSelections.push( new LanguageSelection() );
pageModel.languageSelections.push( new LanguageSelection() );
ko.applyBindings( pageModel );
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<h4>When I update it here</h4>
<ul data-bind="foreach: languageSelections">
<li>
<select data-bind="options: $parent.availableLanguages, value: language"></select>
</li>
</ul>
<h4>I want to see the update here</h4>
<ul data-bind="foreach: languageSelections">
<li data-bind="text: language"></li>
</ul>
I've ended up with ugly solution, but it works. There should be more elegant way. Here is the link:
http://jsfiddle.net/Lypmnspz/10/
HTML
<h4>When I update it here</h4>
<ul data-bind="foreach: languages">
<li><select data-bind="options: $parent.availableLanguages, value: $data, event: {change: $parent.updateOtherLanguage}, attr: {id: 'langopt-'+$index() }"></select></li>
</ul>
<h4>I want to see the update here</h4>
<ul data-bind="foreach: languages">
<li data-bind="text: $data"></li>
</ul>
Javascript
function pageModel(){
var self = this
// Create an observalbe array of options
self.languages = ko.observableArray([ko.observable("English"),ko.observable("English"),ko.observable("English")]);
// Languages
self.availableLanguages = ko.observableArray(["English", "Spanish", "German", "Russian"]);
self.updateOtherLanguage = function(lang, event){
// console.log(event.target.id.split('-')[1],event.target.value)
self.languages()[Number(event.target.id.split('-')[1])]((event.target.value))
}
}
ko.applyBindings(new pageModel());

Not able to bind static data using Knockout JS

So, I load my page and I have my list items with their unique id's.
<ul class="list-group" data-bind="template: { name: 'item-template', data: $root.items}">
<li class="list-group-item">
<span id="123" data-bind="text: item_name, attr: {'id': item_id}">Americanino</span>
<span class="glyphicon glyphicon-remove-circle" data-bind="click: $parent.removeItem"></span>
</li>
<li class="list-group-item">
<span id="223" data-bind="text: item_name, attr: {'id': item_id}">Asos</span>
<span class="glyphicon glyphicon-remove-circle" data-bind="click: $parent.removeItem"></span>
</li>
</ul>
And I bind the attributes after page load
jQuery(document).ready(function($) {
var itemListModel = function() {
(...)
self.item_id = ko.observable();
(...)
}
ko.applyBindings(new itemListModel());
});
But when I try to remove a list item that is loaded from server
// Remove item
self.removeItem = function(item) {
alert(self.item_id());
//self.items.remove(item);
}
then I'm not able to retrieve the ID.
If I add a new item, then I can get the ID. But then I also get the same ID if I click any other list item as well.
So how can I bind "static" content?
Is it a problem that I have a hidden output that also has data-bind="value: item_id"?
See my fiddle here
Modify your create_list method
function create_list(exiting_list){
var arr_list = [];
$(exiting_list).find('li').each(function(e,li){
var id = $(li).find('span').prop('id');
var name = $(li).find('span').html();
arr_list.push(new item(id, name));
});
return arr_list;
}
Updated fiddle here - http://jsfiddle.net/sherin81/JF55A/10/
Implemented the solution for sort .. updated Fiddle - http://jsfiddle.net/sherin81/JF55A/11/

Durandal widgets, dynamic templated parts

I'm building a wizard widget with Durandal, and I'd like to use it like so:
<div data-bind="wizard: options">
<!-- Step 1 -->
<span data-part="step-header-1">
Step 1
</span>
<div data-part="step-content-1">
step content here
</div>
<!-- Step 2 -->
<span data-part="step-header-2">
Step 2
</span>
<div data-part="step-content-2">
step content here
</div>
</div>
This is the actual widget (cut down for brevity):
<div class="wizard-container">
<ul class="steps" data-bind="foreach: steps">
<li>
<span data-bind="html: heading"></span>
</li>
</ul>
<!-- ko foreach: steps -->
<div class="wizard-step" data-bind="css: { active: isActive }">
<div data-bind="html: content">
</div>
</div>
<!-- /ko -->
</div>
I've sort of gotten it working, using jQuery to grab the data-parts, assign the data-part's inner HTML to a property on my step model, and then use the html-binding to bind the content to each step. This works on the DOM side of things, but doing it this way means that my step content won't get data-bound.. I am pretty sure it's because I use the html binding, which does not bind the content.
Is there a way to do this with Durandal widgets, without separating each step into a new view?
Here's an implementation that uses a traditional Durandal master/detail approach in combination with a Tab widget. The tab widget only implements the tabbing functionality, while the Master controls what's pushed into it and the Detail controls the behavior/layout of itself.
Master
Viewmodel
define(['./tab', 'plugins/widget', 'knockout'], function (Tab, widget, ko) {
return {
tabs: ko.observableArray([
new Tab('Durandal', 'A ...', true),
new Tab('UnityDatabinding', 'A ...'),
new Tab('Caliburn.Micro', 'C ...')
]),
addNewTab: function() {
this.tabs.push(new Tab('New Tab ', 'A test tab.'));
}
};
});
View
<div>
<h1>Tabs sample</h1>
<!-- ko widget : {kind: 'tabs', items : tabs} -->
<!-- /ko -->
<button class="btn" data-bind="click: addNewTab">Add</button>
</div>
Detail
Viewmodel
define(['durandal/events', 'knockout'], function(events, ko) {
return function(name, content, isActive) {
this.isActive = ko.observable(isActive || false);
this.name = name;
this.content = content;
};
});
view
<div>
<div data-bind="html: description"></div>
</div>
Tab widget
Viewmodel
define(['durandal/composition', 'jquery'], function(composition, $) {
var ctor = function() { };
ctor.prototype.activate = function(settings) {
this.settings = settings;
};
ctor.prototype.detached = function() {
console.log('bootstrap/widget/viewmodel: detached', arguments, this);
};
ctor.prototype.toggle = function(model, event){
this.deactivateAll();
model.isActive(true);
};
ctor.prototype.deactivateAll = function(){
$.each(this.settings.items(), function(idx, tab){
tab.isActive(false);
});
};
return ctor;
});
View
<div class="tabs">
<ul class="nav nav-tabs" data-bind="foreach: { data: settings.items }">
<li data-bind="css: {active: isActive}">
<a data-bind="text: name, click: $parent.toggle.bind($parent)"></a>
</li>
</ul>
<div class="tab-content" data-bind="foreach: { data: settings.items}">
<div class="tab-pane" data-bind="html: content, css: {active: isActive}"></div>
</div>
</div>
Live version available at: http://dfiddle.github.io/dFiddle-2.0/#extras/default. Feel free to fork.
As I suspected, the problem with my bindings not applying, was due to the fact that I used the html binding to set the step content. When Knockout sets the HTML, it does not apply bindings to it.
I wrote my own HTML binding handler, that wraps the HTML and inserts it as a DOM-node - Knockout will hapily apply bindings to this.
(function(window, $, ko) {
var setHtml = function (element, valueAccessor) {
var $elem = $(element);
var unwrapped = ko.utils.unwrapObservable(valueAccessor());
var $content = $(unwrapped);
$elem.children().remove().end().append($content);
};
ko.bindingHandlers.htmlAsDom = {
init: setHtml,
update: setHtml
};
}(window, jQuery, ko));
Please note, this only works when the binding value is wrapped as a node - e.g within a div tag. If not, it won't render it.

Categories