knockout 'click' binding not working - javascript

I am trying to bind a click event to a list item. When I click on the list item below, it should fire up the updateCurrent function in the javascript, but it doesn't do anything.
note: I have used $parent.updateCurrent as I am in the currentCat context when I call the function and the updateCurrent function is in the ViewModel context.
HTML:
<div id="wrap" data-bind="with: currentCat">
<div id="names">
<ul data-bind="foreach: $parent.catNames">
<!--Here, clicking on name doesn't fire updateCurrent-->
<li data-bind="text: $data, click: $parent.updateCurrent"></li>
</ul>
</div>
</div>
JS:
var viewModel = function() {
var self = this;
this.catNames = ko.observableArray([]);
this.catList = ko.observableArray([]);
//cats is defined as a simple array of objects, having name
cats.forEach(function(catObject) {
self.catList.push(new cat(catObject));
self.catNames.push(catObject.name);
});
//initially, set the currentCat to first cat
this.currentCat = ko.observable(this.catList()[0]);
//should run when the user click on the cat name in the list
this.updateCurrent = function() {
console.log("hello");
}
};
I don't get any error when I click on the list name, but console doesn't log anything.
I believe I am not using the context properly, but can't figure out the exact issue here. Any help will be appreciated.

When you say $parent.catNames, you are referring to the ViewModel because it is an owned property of the immediate parent in the hierarchy. With the foreach binding, you are within a new context; therefore in $parent.updateCurrent, the immediate parent in the hierarchy becomes the item in catNames array.
You can use $root in this case as Viktor has mentioned but the safest and proper way to achieve this functionality is to climb the hierarchy one level at a time to obtain $parents[1].
$parent is equal to $parents[0] which is one of the catNames. $parents[1] will refer to the ViewModel.
Why not use $root?
Knockout will maintain your DOM and binding context much deeper than may be readily apparent. In template bindings, the $root context may refer to an object that isn't even in the HTML template!
<div data-bind="template: { name: 'sample', data: catNames }"></div>
<script type='text/html' id='sample'>
<!-- ko foreach: $data -->
<span data-bind="text: 'Cat ' + $index + ' of ' + $parents[0].length"></span>
Returns "Cat 1 of 10"
<!-- /ko -->
<!-- ko foreach: $data -->
<span data-bind="text: 'Cat ' + $index + ' of ' + $root.length"></span>
Return "Cat 1 of undefined" because ViewModel has no length property!
<!-- /ko -->
</script>
Binding context documentation: http://knockoutjs.com/documentation/binding-context.html

You are inside two levels of context. The with binding indicates that $data inside that div is currentCat. The foreach provides another level of context. $parent doesn't refer to the parent of an object, but the context outside this context. You could use $parents[1].
When you say $parent.catNames, you're only inside the with context, so it does refer to catNames in ViewModel, since there's no other enclosing context. But where you say $parent.updateCurrent, you're jumping out of the foreach context up to the with context. And you wanted to jump one more.

Related

KoJs: bind a dynamic number of text boxes to elements of an array

I have a front-end which allows for adding and removing of text boxes suing the foreach binding. A text box looks something like this
<div id="dynamic-filters" data-bind="foreach: filterList">
<p>
<input type="text" data-bind="textInput: $parent.values[$index()], autoComplete: { options: $parent.options}, attr: { id : 'nameInput_' + $index() }"/>
</p>
</div>
What I want to do, as shown in the code above is to bind each of these dynamically generated text boxes to an element in the array using the $index() context provided by knockout.js
However it doesn't work for me, my self.values=ko.observableArray([]) doesn't change when the text boxes change.
My question is, if I want to have a way to bind these dynamically generated text boxes, is this the right way to do it? If it is how do I fix it? If it's not, what should I do instead?
Thanks guys!
EDIT 1
the values array is an observable so I thought I should unwrap it before use. I changed the code to
<input type="text" data-bind="textInput: $parent.values()[$index()], autoComplete: { options: $parent.options}, attr: { id : 'nameInput_' + $index() }"/>
This works in a limited way. When I add or change the content of text boxes, the array changes accordingly. However when I delete an element it fails in two ways:
If I delete the last item, the array simply doesn't change
If I delete an item in between, everything is shifted back
I suppose I have to add a function that changes the text-input value before destroying the text box itself.
Any help or advice on how to do this?
I would suggest taking the array of values and mapping it to some kind of model first, then dumping it into the filterList ko.observableArray. It can be as complex or as simple as need be.
That way you have direct access to those properties at the ko foreach: level instead of having to do the goofy index access.
I've added a simple knockout component example as well to show you what can be achieved.
var PageModel = function() {
var self = this;
var someArrayOfValues = [{label: 'label-1', value: 1},{label: 'label-2', value: 2},{label: 'label-3', value: 3},{label: 'label-4', value: 4}];
this.SimpleInputs = ko.observableArray(_.map(someArrayOfValues, function(data){
return new SimpleInputModel(data);
}));
this.AddSimpleInput = function(){
self.SimpleInputs.push(new SimpleInputModel({value:'new val', label:'new label'}));
};
this.RemoveSimpleInput = function(obj){
self.SimpleInputs.remove(obj);
}
}
var SimpleInputModel = function(r) {
this.Value = ko.observable(r.value);
this.Label = r.label;
};
var SimpleInputComponent = function(params){
this.Id = makeid();
this.Label = params.label;
this.Value = params.value;
function makeid() {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (var i = 0; i < 5; i++)
text += possible.charAt(Math.floor(Math.random() * possible.length));
return text;
}
}
ko.components.register('input-component', {
viewModel: SimpleInputComponent,
template: '<label data-bind="text: Label, attr: {for: Id}"></label><input type="text" data-bind="textInput: Value, attr: {id: Id}" />'
})
window.model = new PageModel();
ko.applyBindings(model);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<!-- ko if: SimpleInputs -->
<h3>Simple Inputs</h3>
<!-- ko foreach: SimpleInputs -->
<input-component params="value: Value, label: Label"></input-component>
<button data-bind="click: $parent.RemoveSimpleInput">X</button>
<br>
<!-- /ko -->
<!-- /ko -->
<button data-bind="click: AddSimpleInput">Add Input</button>
EDIT (7/16/2020):
Mind explaining this without requiring lodash? I literally googled "how to lodash map using plain javascript". Excellent answer otherwise! – CarComp
In this scenario the lodash _.map method could be overkill unless you are executing the script in an environment that does not have native support for the vanilla array map method. If you have support for the vanilla method, go ahead and use that. The map method essentially iterates over each array using the method it is handed to return a transformed array of the original items. Implementation of vanilla code would look like so.
this.SimpleInputs = ko.observableArray(someArrayOfValues.map(function(data) {
return new SimpleInputModel(data);
}));
Here we are taking the values of someArrayOfValues and telling it to use each item to build a new SimpleInputModel and return it using that item data. [SimpleInputModel, SimpleInputModel, SimpleInputModel, SimpleInputModel] is what the new array turns into after mapping. Each of these items has all the functionality described in the SimpleInputModel class, Value and Label.
So with the new array you could, if you wanted, access the values like this as well self.SimpleInputs[0].Value() or self.SimpleInputs[0].Label
Hope that helps to clarify.

binding to controller object in Angular

I'm new to angular, trying to bind an an element's content into the controller's Scope to be able to use it within another function:
here is the scenario am working around:
I want the content of the <span> element {{y.facetName}} in
<span ng-model="columnFacetname">{{y.facetName}}</span>
to be sent to the controller an be put in the object $scope.columnFacetname in the controller
Here is a snippet of what I'm working on:
<div ng-repeat="y in x.facetArr|limitTo: limit track by $index ">
<div class="list_items panel-body ">
<button class="ButtonforAccordion" ng-click="ListClicktnColumnFilterfunc(); onServerSideButtonItemsRequested(ListClicktnColumnFilter, myOrderBy)">
<span>{{$index+1}}</span>
<span ng-model="columnFacetname">{{y.facetName}}</span>
<span>{{y.facetValue}}</span>
</button>
</div>
</div>
angular.module('mainModule').controller('MainCtrl', function($scope, $http) {
$scope.columnFacetname = "";
$scope.ListClicktnColumnFilter = "";
$scope.ListClicktnColumnFilterfunc = function() {
$scope.ListClicktnColumnFilter = "\":\'" + $scope.columnFacetname + "\'";
};
}
the problem is that the $scope.ListClicktnColumnFilter doesn't show the $scope.columnFacetname within it, meaning that the $scope.columnFacetname is not well-binded.
In your ng-click instead of calling two different function
ng-click="ListClicktnColumnFilterfunc(); onServerSideButtonItemsRequested(ListClicktnColumnFilter, myOrderBy)"
you can declare like this
ng-click="columnFacetname = y.facetName; onServerSideButtonItemsRequested(columnFacetname , myOrderBy)"
You are trying to pass that model to another function by assigning it to ListClicktnColumnFilter in your controller
By doing in this way, you can achieve the same thing.
I have done one plunker with sample array,
http://embed.plnkr.co/YIwRLWXEOeK8NmYmT6VK/preview
Hope this helps!

Accessing objects within viewmodel Knockout

I'm using knockout for a single page app that does some basic calculations based on several inputs to then populate the value of some html . In an attempt to keep my html concise I've used an array of objects in my viewModel to store my form. I achieved the basic functionality of the page however I wish to add a 'display' value to show on html that has formatted decimal points and perhaps a converted value in the future.
I'm not sure of a 'best practices' way of accessing the other values of the object that I'm currently 'in'. For example: If I want my display field to be a computed value that consists of the value field rounded to two decimal places.
display: ko.computed(function()
{
return Math.round(100 * myObj.value())/100;
}
I've been reading through the documentation for knockout and it would appear that managing this is a common problem with those new to the library. I believe I could make it work by adding the computed function outside of the viewmodel prototype and access the object by
viewModel.input[1].value()
However I would imagine there is a cleaner way to achieve this.
I've included a small snippet of the viewModel for reference. In total the input array contains 15 elements. The HTML is included below that.
var ViewModel = function()
{
var self = this;
this.unitType = ko.observable('imperial');
this.input =
[
{
name: "Test Stand Configuration",
isInput: false
},
{
name: "Charge Pump Displacement",
disabled: false,
isInput: true,
unitImperial: "cubic inches/rev",
unitMetric: "cm^3/rev",
convert: function(incomingSystem)
{
var newValue = this.value();
if(incomingSystem == 'metric')
{
//switch to metric
newValue = convert.cubicinchesToCubiccentimeters(newValue);
}
else
{
//switch to imperial
newValue = convert.cubiccentimetersToCubicinches(newValue);
}
this.value(newValue);
},
value: ko.observable(1.4),
display: ko.computed(function()
{
console.log(self);
}, self)
}
]
};
__
<!-- ko foreach: input -->
<!-- ko if: $data.isInput == true -->
<div class="form-group">
<div class="col-sm-6">
<label class="control-label" data-bind="text: $data.name"></label>
</div>
<div class="col-sm-6">
<div class="input-group">
<!-- ko if: $data.disabled == true -->
<input data-bind="value: $data.value" type="text" class="form-control" disabled>
<!-- /ko -->
<!-- ko if: $data.disabled == false -->
<input data-bind="value: $data.value" type="text" class="form-control">
<!-- /ko -->
<!-- ko if: viewModel.unitType() == 'imperial'-->
<span data-bind="text: $data.unitImperial" class="input-group-addon"></span>
<!-- /ko -->
<!-- ko if: viewModel.unitType() == 'metric' -->
<span data-bind="text: $data.unitMetric" class="input-group-addon"></span>
<!-- /ko -->
</div>
</div>
</div>
<!-- /ko -->
<!-- ko if: $data.isInput == false -->
<div class="form-group">
<div class="col-sm-6">
<h3 data-bind="text: $data.name"></h3>
</div>
</div>
<!-- /ko -->
If you want to read/ write to & from the same output, #Aaron Siciliano's answer is the way to go. Else, ...
I'm not sure of a 'best practices' way of accessing the other values of the object that > I'm currently 'in'. For example: If I want my display field to be a computed value that consists of the value field rounded to two decimal places.
I think there's a misconception here about what KnockoutJS is. KnockoutJS allows you to handle all your logic in Javascript. Accessing the values of the object you are in is simple thanks to Knockout's context variables: $data (the current context, and the same as JS's this), $parent (the parent context), $root(the root viewmodel context) and more at Binding Context. You can use this variables both in your templates and in your Javascript. Btw, $index returns the observable index of an array item (which means it changes automatically when you do someth. wth it). In your example it'd be as simple as:
<span data-bind="$data.display"></span>
Or suppose you want to get an observable w/e from your root, or even parent. (Scenario: A cost indicator that increases for every item purchased, which are stored separately in an array).
<span data-bind="$root.totalValue"></span>
Correct me if I'm wrong, but given that you have defined self only in your viewmodel, the display function should output the whole root viewmodel to the console. If you redefine a self variable inside your object in the array, self will output that object in the array. That depends on the scope of your variable. You can't use object literals for that, you need a constructor function (like the one for your view model). So you'd get:
function viewModel() {
var self = this;
self.inputs = ko.observableArray([
// this builds a new instance of the 'input' prototype
new Input({initial: 0, name: 'someinput', display: someFunction});
])
}
// a constructor for your 15 inputs, which takes an object as parameter
function Input(obj) {
var self = this; // now self refers to a single instance of the 'input' prototype
self.initial = ko.observable(obj.initial); //blank
self.name = obj.name;
self.display = ko.computed(obj.fn, this); // your function
}
As you mentioned, you can also handle events afterwards, see: unobtrusive event handling. Add your event listeners by using the ko.dataFor & ko.contextFor methods.
It appears as though KnockoutJS has an example set up on its website for this exact scenario.
http://knockoutjs.com/documentation/extenders.html
From reading that page it looks as though you can create an extender to intercept an observable before it updates and apply a function to it (to format it for currency or round or perform whatever changes need to be made to it before it updates the ui).
This would probably be the closest thing to what you are looking for. However to be completely honest with you i like your simple approach to the problem.

this in event handling functions of Meteor: How does this get bound to model object?

The following code is taken from a tutorial in tutsplus.
if (Meteor.isClient) {
var Products = new Array(
{ Name: "Screw Driver",
Price: "1.50",
InStock: true},
{ Name: "Hammer",
Price: "2.50",
InStock: false}
);
Template.Products.ProductArr = function () {
return Products;
};
Template.Products.events = {
"click .Product": function () {
if (this.InStock)
confirm("Would you like to buy a " + this.Name + " for " + this.Price + "$");
else
alert("That item is not in stock");
}
};
}
Here is the template:
<template name="Products">
{{#each ProductArr}}
<div class="Product">
<h2>{{Name}}</h2>
<p>Price: ${{Price}}</p>
{{#if this.InStock}}
<p>This is in stock</p>
{{else}}
<p>This is sold out</p>
{{/if}}
</div>
{{/each}}
</template>
I wonder how this get bound to the model object product? This looks like magic to me.
The expression "click .Product" specifies that the click event on HTML elements having class Product should trigger the specified function. I understand it. But I don't understand why this is bound to an element of the Products array.
This is how Handlebars (which Meteor builds on) works. What you're seeing in the template isn't pure JS, but syntax specific to Handlebars.
Inside the each block helper, the context is to set each element of the array you're iterating over. So if you use InStock, it will look for it on the element of the current iteration.
The this keyword is used for disambiguation. This comes in handy if you have a general helper registered with the name InStock, like this, for example:
Template.Products.InStock = function (){
//...
};
But you want to make sure you're referring to the property of the element from the array, so you can use this to access its context explicitly.

handlebars - is it possible to access parent context in a partial?

I've got a handlebar template that loads a partial for a sub-element.
I would need to access a variable from the parent context in the calling template, from within the partial. .. doesn't seem to resolve to anything inside the partial.
Simplified code goes like this:
the template
{{#each items}}
{{> item-template}}
{{/each}}
the partial
value is {{value}}
(obviously the real code is more complicated but it's the same principle, within the partial .. appears to be undefined.)
To show it's undefined, I've used a very simple helper whatis like this:
Handlebars.registerHelper('whatis', function(param) {
console.log(param);
});
and updated the above code to this:
updated template
{{#each items}}
{{whatis ..}} <-- Console shows the correct parent context
{{> item-template}}
{{/each}}
updated partial
{{whatis ..}} <-- Console shows "undefined"
value is {{value}}
Is there a way to go around that issue? Am I missing something?
EDIT: There's an open issue relating to this question on handlebars' github project
Just in case anyone stumbles across this question. This functionality exists now in Handlebars.
Do this:
{{#each items}}
{{! Will pass the current item in items to your partial }}
{{> item-template this}}
{{/each}}
Working fiddle (inspired by handlebars pull request #385 by AndrewHenderson)
http://jsfiddle.net/QV9em/4/
Handlebars.registerHelper('include', function(options) {
var context = {},
mergeContext = function(obj) {
for(var k in obj)context[k]=obj[k];
};
mergeContext(this);
mergeContext(options.hash);
return options.fn(context);
});
Here's how you'd setup the parent template:
{{#each items}}
{{#include parent=..}}
{{> item-template}}
{{/include}}
{{/each}}
And the partial:
value is {{parent}}
As of 2.0.0 partials now supports passing in values.
{{#each items}}
{{> item-template some_parent_var=../some_parent_var}}
{{/each}}
Took me awhile to find this, hope it's useful for someone else too!
The easiest way to pass the parent context to the partial is to do the loop inside the partial. This way the parent context is passed by default and when you do the loop inside the partial the {{../variable}} convention can access the parent context.
example fiddle here.
The Data
{
color: "#000"
items: [
{ title: "title one" },
{ title: "title two" },
]
}
The Template
<div class="mainTemplate">
Parent Color: {{color}}
{{> partial}}
</div>
The Partial
<div>
{{#each items}}
<div style="color:{{../color}}">
{{title}}
</div>
{{/each}}
</div>
You can use some of the proposed solutions on the comments from the link to github:
https://github.com/wycats/handlebars.js/issues/182#issuecomment-4206666
https://github.com/wycats/handlebars.js/issues/182#issuecomment-4445747
They create helpers to pass the info to the partial.
I created an each Helper function that includes the parent key/values within the subcontext under the key parentContext.
http://jsfiddle.net/AndrewHenderson/kQZpu/1/
Note: Underscore is a dependency.
Handlebars.registerHelper('eachIncludeParent', function ( context, options ) {
var fn = options.fn,
inverse = options.inverse,
ret = "",
_context = [];
$.each(context, function (index, object) {
var _object = $.extend({}, object);
_context.push(_object);
});
if ( _context && _context.length > 0 ) {
for ( var i = 0, j = _context.length; i < j; i++ ) {
_context[i]["parentContext"] = options.hash.parent;
ret = ret + fn(_context[i]);
}
} else {
ret = inverse(this);
}
return ret;
});
To be used as follows:
{{#eachIncludeParent context parent=this}}
{{> yourPartial}}
{{/eachIncludeParent}}
Access parent context values in your partial using {{parentContext.value}}
I needed dynamic form attributes for something like this...
{{#each model.questions}}
<h3>{{text}}</h3>
{{#each answers}}
{{formbuilder ../type id ../id text}}
{{/each}}
{{/each}}
and a helper like so...
Handlebars.registerHelper('formbuilder', function(type, id, qnum, text, options)
{
var q_type = options.contexts[0][type],
a_id = options.contexts[1].id,
q_number = options.contexts[0][qnum],
a_text = options.contexts[1].text;
return new Handlebars.SafeString(
'<input type=' + q_type + ' id=' + a_id + ' name=' + q_number + '>' + a_text + '</input><br/>'
);
});
Which produces...
<input type="checkbox" id="1" name="surveyQ0">First question</input>
My model is a big blob of arrays and objects mixed together. What's noteworthy is that using '../' like so '../type', passes in the parent model as the context, and without it, such as with 'id', it passes in the current model as the context.
To get specifically the parent of the partial (where you may be several partials deep) then follow the other answers like SeanWM.
If you know that the parent is the main template then you can use #root which resolves to the top-most context no matter how deep you are.
e.g. {{#root.rootObject.rootProperty}}
It is a pity that ../../.. does not go up past a partial.

Categories

Resources