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>
Related
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.
I'm calling two times the same list in my HTML5 view.
<div class="list" data-ng-repeat="item in model.items">
<div class=list-item">
{{ item.name }}
<a data-ng-click="addToList(item)">
<span data-ng-hide="item.checked">Add</span>
<span data-ng-show="item.checked">Remove</span>
</a>
</div>
</div>
<div class="checkbox-list" data-ng-repeat="item in model.items">
<div class=list-item">
<input type="checkbox" data-ng-checked="item.checked" data-ng-click="addToList(item)" />
{{ item.name }}
</div>
</div>
In my controller, I'm having the method defined in the scope:
function addToList(item) {
item.checked = !item.checked;
for (var i = 0; i < this.$scope.items.length; i++) {
if (this.$scope.items.id == item.id) {
this.$scope.items[i] = item;
break;
}
}
}
When I click the Addor Remove button, the list has an update, but the checkbox-list item doesn't get checked.
On the other hand, when checking a checkbox, the list doesn't get an update.
How can I make this happen?
Looks like you are using controller as syntax.
So in your js code add the model to the controller itself(assuming you yourController refers to the controller and you have your methods in the controller):
for (var i = 0; i < yourController.items.length; i++) {
if (yourController.items.id == item.id) {
yourController.items[i] = extension;
break;
}
}
Inside YourController declare:
var yourController = this;
Then in the HTML where you declare the controller, declare it as follows:
ng-controller = "YourController as yourController"
And then you can access the items in HTML as:
yourController.items
you are mixing the javascript function declaration syntax with angular
While working with angular you should always angular syntax to get the benefit of synchronization between the model and the view.
$scope.addToList = function(){}
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.model = {};
$scope.model.items = [{"name":"one"},{"name":"two"}]
$scope.addToList = function(item) {
item.checked = !item.checked;
}
});
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<link rel="stylesheet" href="style.css" />
<script data-require="angular.js#1.4.x" src="https://code.angularjs.org/1.4.12/angular.js" data-semver="1.4.9"></script>
<script src="app.js"></script>
</head>
<body ng-controller="MainCtrl">
<div class="list" data-ng-repeat="item in model.items">
<div class="list-item">
{{ item.name }}
<a data-ng-click="addToList(item)">
<span data-ng-hide="item.checked">Add</span>
<span data-ng-show="item.checked">Remove</span>
</a>
</div>
</div>
<div class="checkbox-list" data-ng-repeat="item in model.items">
<div class="list-item">
<input type="checkbox" data-ng-checked="item.checked" data-ng-click="addToList(item)" />
{{ item.name }}
</div>
</div>
</body>
</html>
You are not using function with $scope. This might help:
$scope.addToList = function (item) {
item.checked = !item.checked;
for (var i = 0; i < $scope.items.length; i++) {
if ($scope.items.id == item.id) {
$scope.items[i] = extension;
break;
}
}
}
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>
I'm creating a set of JavaScript methods that will clone pre-existing HTML. Here is the HTML:
<!DOCTYPE html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<!-- <script src="lib/pm-helpers.js"></script> -->
<script src="lib/pm-ajax-inputs.js"></script>
</head>
<style>
.hidden { display: none; }
</style>
<body>
<script>
jQuery( document ).ready( function() {
/*
* Clone example
* the instance() method takes 3 parameters
* #param: wrapper id or class
* #param: clone group
* #param: action button, image or link; something with a click event ability.
*/
var instance = PM_CONTROLS.MODEL.PROPS.instance( '.first', '.control-group', '#clone' );
instance.bind();
} );
</script>
<ul class='first'>
<li class='control-group'>
<input type="text" name="discover[]" value="" placeholder="info here" />
<input type="button" id="bn-edit" class="green pm-edit" value="edit">
<input type="button" id="bn-delete" class="blue pm-delete" value='delete'>
</li>
</ul>
<ul class='second'>
<li id='control-group2' class='controls '>
<input type="text" name="discover[]" value="" placeholder="info here" />
<input type="button" id="bn-edit1" class="green pm-edit" value="farfenugal">
<input type="button" id="bn-delete1" class="blue pm-delete" value='farfenay'>
</li>
</ul>
<ul id="control-wrapper">
<li class='controls'>
<input type="text" name="discover[]" value="" placeholder="control purpose here" />
<input type="button" id="clone" class="green pm-add" value="add">
</li>
</ul>
<ul id="control-wrapper2">
<li class='controls'>
<input type="text" name="discover[]" value="" placeholder="control purpose here" />
<input type="button" id="clone1" class="green pm-add" value="add">
</li>
</ul>
</body>
Here is the JS:
var PM_CONTROLS = PM_CONTROLS || {};
PM_CONTROLS.createNS = function ( namespace ) {
var nsparts = namespace.split(".");
var parent = PM_CONTROLS;
// include or exclude the root namespace so we strip it if it's in the namespace
if (nsparts[0] === "PM_CONTROLS") {
nsparts = nsparts.slice(1);
}
// if required create a nested namespace by looping through the parts
for (var i = 0; i < nsparts.length; i++) {
var partname = nsparts[i];
// check if the current parent already has the namespace declared
// if it doesn't then create it
if (typeof parent[ partname ] === "undefined") {
parent[partname] = {};
}
// get a reference to the deepest element in the hierarchy
parent = parent[ partname ];
}
// the parent is now constructed with empty namespaces and can be used.
// we return the outermost namespace
return parent;
};
PM_CONTROLS.createNS( 'PM_CONTROLS.MODEL.PROPS');
PM_CONTROLS.createNS( 'PM_CONTROLS.ACTIONS');
PM_CONTROLS.props = null;
PM_CONTROLS.i= 0;
PM_CONTROLS.MODEL.PROPS.instance = function ( wrapper_class_or_id, element_to_clone_id, clone_button_id ) {
var props = {
wrapper : wrapper_class_or_id + PM_CONTROLS.i++
, clone : element_to_clone_id + PM_CONTROLS.i
, button : clone_button_id
};
var bind = function() {
PM_CONTROLS.props = getProps();
return new PM_CONTROLS.ACTIONS.bindAction();
};
var getProps = function() {
return props;
};
return {
bind : bind
, getProps: getProps
};
};
PM_CONTROLS.ACTIONS.clone = function() {
var clone = $( PM_CONTROLS.props.clone ).clone();
$( PM_CONTROLS.props.wrapper ).append( clone );
clone.fadeIn('fast');
};
PM_CONTROLS.ACTIONS.bindAction = function () {
$( PM_CONTROLS.props.button ).on( 'click', '', PM_CONTROLS.ACTIONS.clone() );
//$( '#clone' ).click( PM_CONTROLS.ACTIONS.clone() );
};
So when running the HTML the ul with the class='control-group' is getting loaded into the PM_CONTROLS closures and properties.
Furthermore, the code steps through everything without any errors and the values that eventually get passed to the bindAction() method are correct ( I've hard-coded the selector for debugging purposes in the bindAction code ).
Can you explain why the HTML button with the id='#clone' is not binding a click event for the instance variable that is created?
Your help is appreciated on this one.
Regards,
Steve
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.