How to bind more than 1 property to element? - javascript

I'm playing around with knockout and I can't figure out how do I bind more than 1 object property to an element?
This is my template and I tried data-bind="text: name.first name.last":
<script type="text/html" id="person-template">
<h3 data-bind="text: name.first name.last"></h3>
<p>Age: <span data-bind="text: age"></span></p>
<p>Company: <span data-bind="text: company"></span></p>
<hr>
</script>
In angular I'd use ng-repeat and do something like <h3>{{name.first}} {{name.last}}</h3>but I don't want to load angular for this project

Like this:
<h3 data-bind="text: name.first + ' ' + name.last"></h3>
or this, if those items are observables:
<h3 data-bind="text: name.first() + ' ' + name.last()"></h3>
or this, if you want to keep things seperate in your view:
<h3 data-bind="with: name">
<span data-bind="text: first"></span>
<span data-bind="text: last"></span>
</h3>
or this, if you want to mimick Angular as close as possible:
<h3>
<!-- ko text: name.first --><!-- /ko -->
<!-- ko text: name.last --><!-- /ko -->
</h3>
or this, if you want to unit test / make it into business logic:
<h3 data-bind="text: name.fullname"></h3>
function Name() {
var self = this;
self.first = ko.observable();
self.last = ko.observable();
self.fullname = ko.computed(function() {
return self.first() + " " + self.last();
});
}
As a footnote, some relevant differences between KnockoutJS and AngularJS:
In Knockout there is (without plugins) no direct equivalent of the AngularJS bracket style (e.g. {{ name.first}}) rendering.
In KnockoutJS observable properties are functions (whereas Angular can track changes to plain JS properties). You can only (optionally) leave off parentheses if it's the only thing you're binding to. So supposing first is an observable, these work:
<span data-bind="text: name.first"></span> shortcut...
<span data-bind="text: name.first()"></span> equivalent to previous...
<span data-bind="text: name.first() + name.last()"></span> () are required now...
but this doesn't:
<span data-bind="text: name.first + 'something else'"></span> !doesn't work!

Related

How to condition appearance of div in Knockout JS based on value from $root?

I want to show/hide a div in my code based on a certain value. Using Knockout JS context debugger I found the property I need to compare in $root context. The property path I get from developer console is :
$root_toJS.items.mainItems[0].itemDescription.productId
I tried several methods, basically different variations of the 'ko if' in the following code, but nothing works:
<!-- ko if: $root.items.mainItems[0].itemDescription.productId != 1 -->
<div class="action-row">
<a href="#" data-bind="click: execute" class="btn-primary fiori3-btn-primary">
<span data-bind="text: name"></span>
</a>
</div>
<!-- /ko -->
Is there any way I can acces the value at the specified path in a 'ko if' condition?
Thank you
when you're referring to your name variable I'm assuming it's in the context of your itemDescription, so you have to make sure you're using the exact location of it also.
In my 2nd example I'm using a foreach loop to go over all the mainItems. Pay attention to the as: mainItem alias where I don't need to enter the entire thing anymore, you could also use $data but that only complicates it imo.
class ViewModel {
constructor() {
this.items = {
mainItems: [{
itemDescription: {
productId: 1,
name: 'item one',
}
}, {
itemDescription: {
productId: 2,
name: 'item two',
}
}]
};
}
};
ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<h5>this breaks any generic approach but is as your example:</h5>
<!-- ko if: $root.items.mainItems[0].itemDescription.productId !== 1 -->
<div>
<span data-bind="text: $root.items.mainItems[0].itemDescription.name"></span>
</div>
<!-- /ko -->
<!-- ko if: $root.items.mainItems[1].itemDescription.productId !== 1 -->
<div>
<span data-bind="text: $root.items.mainItems[1].itemDescription.name"></span>
</div>
<!-- /ko -->
<h5>this embraces it what is probably more what you'd want:</h5>
<!-- ko foreach: { data: $root.items.mainItems, as: 'mainItems' } -->
<!-- ko if: mainItems.itemDescription.productId !== 1 -->
<div>
<span data-bind="text: mainItems.itemDescription.name"></span>
</div>
<!-- /ko -->
<!-- /ko -->

Knockout Prototype Issue

I would like to understand why knockout behaves differently when a property is a prototype, and especially important, how to avoid it, while still using prototypes. I want some methods to be overridden because I have a base view model that we are to inherit
The following demonstrates what I mean
JSFIDDLE WITH PROTOTYPE [try typing into the first input box and it will appear in the other]
var viewModel = function(params) {
this.params = params;
};
viewModel.prototype.text = ko.observable(this.params && this.params.initialText || '');
ko.components.register('message-editor', {
viewModel: viewModel,
template: 'Message: <input data-bind="value: text" /> '
+ '(length: <span data-bind="text: text().length"></span>)'
});
ko.applyBindings();
<!-- ko component: "message-editor" -->
<!-- /ko -->
<br />
<!-- ko component: "message-editor" -->
<!-- /ko -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
JSFIDDLE WITHOUT PROTOTYPE [try typing into the first input box and it will not appear in the other]
var viewModel = function(params) {
this.params = params;
this.text = ko.observable(this.params && this.params.initialText || '');
};
ko.components.register('message-editor', {
viewModel: viewModel,
template: 'Message: <input data-bind="value: text" /> '
+ '(length: <span data-bind="text: text().length"></span>)'
});
ko.applyBindings();
<!-- ko component: "message-editor" -->
<!-- /ko -->
<br />
<!-- ko component: "message-editor" -->
<!-- /ko -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
Why are they behaving differently?
In JavaScript, the prototype is used only for functions that are common to each object of that type. All instances can share the same copy of the function because the function, when called, gets the reference to the instance object through this. Knockout observables are not designed to be stored in the prototype because they store data specific to an object instance.

Use single observable for multiple rows in table

<div class="tbody" data-bind="foreach: displayItems">
<div class="t-row">
<div class="t-cell">
<div class="manage-location-buttons">
<a href="javascript:void(0)">
<i class="fa fa-pencil" aria-hidden="true" data-bind="toggleClick: $component.openEditPopup"></i> Edit
</a>
<div class="edit-table-popup" data-bind="visible: $component.openEditPopup">
<ul>
<li><a data-hash="#locationmanagement/managelocations/locationediting" data-bind="click: goToTab">Locations</a></li>
<li><a data-hash="#locationmanagement/managelocations/events" data-bind="click: goToTab">Events</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
It is my sample of custom table.
On Link click I will show edit-table-popup div like popup. Cause I use only one observable openEditPopup for all items, onclick I see popup for each row.
openEditPopup = ko.observable<boolean>(false);
toggleClick - is custom dirrective, which changes boolean value to opposite
Is it possible to use only one observable but to show popup only for clicked row?
Yes, it's possible.
The click binding sends two arguments to an event handler:
The clicked $data,
The click-event.
If your click handler is an observable, this means it calls the observable like so: yourObservable(data, event)
Knowing that an observable is set by calling it with an argument, you can imagine what happens. Note that knockout ignores the second argument.
The solution would therefore be to change the openEditPopup from containing a bool, to containing a displayItem, and changing the visible binding to:
visible: $component.openEditPopup() === $data
An example:
var vm = {
selected: ko.observable("A"),
items: ["A", "B", "C", "D"]
};
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<p>Tabs</p>
<div data-bind="foreach: items">
<span data-bind="text: $data, click: $parent.selected"></span>
</div>
<p>Content</p>
<div data-bind="foreach: items">
<div data-bind="visible: $parent.selected() === $data">
<h1 data-bind="text:$data"></h1>
</div>
</div>
If I understand correctly, all your rows are bound to one observable and so when you click the row it is setting it to true and all pop ups are showing up?
If so, this would suggest you have a popup per row? I'd recommend changing this and having one popup that is toggled per row and then set it's data to the selected row. Something like this can be achieved with the code below.
var viewModel = function() {
var rows = ko.observableArray([
{id: 1, name: "gimble"}, {id: 2, name: "bob"}, {id: 3, name: "jess"}
]);
var selectedRow = ko.observable();
function showRowPopup(row)
{
//console.log(row.id);
if(selectedRow() == row)
selectedRow(null);
else
selectedRow(row);
}
return {
rows: rows,
showRowPopup: showRowPopup,
selectedRow: selectedRow
}
}
ko.applyBindings(new viewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="foreach: rows">
<div>
<span data-bind="text: id"></span> -
<span data-bind="text: name"></span>
<button data-bind="click: $parent.showRowPopup">Toggle Modal</button>
</div>
</div>
<div data-bind="with: selectedRow">
<h3>My Fake Modal</h3>
<span data-bind="text: id"></span> -
<span data-bind="text: name"></span>
</div>

Knockout save variable inside template

I have a jquery knockout template which renders a foreach.
Inside each item, i am using the same function many times (for css binding and for visibility of other child elements)
Is it possible that instead of calling the same function many times for each item in the foreach, to temporary save it and then re-use it inside the template?
PS: i know i can set the visibility of the <i class="fa " /> tags using css selectors, but this doesn't answers the question.
<script type="text/html" id="properties-template">
<!-- ko foreach: Groups -->
<!-- saving the result in a variable instead of calling it so many times -->
<!-- var isValid = isGroupValid($data); !-->
<div class="group" data-bind="css: { 'valid': isGroupValid($data) }">
<div class="iconContainer">
<!-- ko if: $root.isGroupValid($data) === false -->
<i class="fa fa-square-o"></i>
<!-- /ko -->
<!-- ko if: $root.isGroupValid($data) === true -->
<i class="fa fa-check-square"></i>
<!-- /ko -->
</div>
</div>
<!-- /ko -->
</script>
As $data is itself the viewmodel, you could just have a computed observable there which does whatever logic isGroupValid currently does. So I assume your view model at the moment looks something like
function ViewModel(){
this.isGroupValid = function(data){
// some logic returning boolean
}
}
Here's a live example demonstrating the "before": http://jsfiddle.net/hbSj7/
change it to this:
function ViewModel(){
this.isGroupValid = ko.computed(function(){
// some logic returning boolean
// just use "this" where you used to use "data"
}, this);
}
Then just change your template to, eg/
<div class="group" data-bind="css: { 'valid': isGroupValid() }">
<div class="iconContainer">
<!-- ko if: !isGroupValid() -->
<i class="fa fa-square-o"></i>
<!-- /ko -->
<!-- ko if: isGroupValid() -->
<i class="fa fa-check-square"></i>
<!-- /ko -->
</div>
</div>
Here's a live example having changed to this method: http://jsfiddle.net/ug9ax/
The important difference is in your current method the function gets executed every time you call it, with the changed method the computed only gets re-evaluated if anything it depends on changes, like other observable properties in your viewmodel.

How to refer to the same parent/grandparent scope with nested loops?

I have nested loops with Knockout. I would like to refer to something in a parent "scope". If you see below I always want to refer to the same parent/grandparent regardless how deep I nest the loops. I have seen the "with" binding, not sure it will help me. Is there any way I can create an alias to a particular scope, so further down in the nested loop I can refer to this alias and still be able to refer to the scope of the current loop also?
<!-- Somewhere up there is the "scope" I want to capture -->
<!-- ko foreach: getPages() -->
<span data-bind="text: pageName" ></span>
<button data-bind="click: $parents[1].myFunction()" >Press me</button>
<!-- ko foreach: categories -->
<span data-bind="text: categoryName" ></span>
<button data-bind="click: $parents[2].myFunction()" >Press me</button>
<!-- ko foreach: questions -->
<span data-bind="text: questionText" ></span>
<button data-bind="click: $parents[3].myFunction()" >Press me</button>
<!-- /ko -->
<!-- /ko -->
<!-- /ko -->
The foreach binding supports as aliases as does the (custom) withProperties binding.
<!-- ko withProperties: { book: $root.getBook() } -->
<!-- ko foreach: {data: book.pages, as: 'page'} -->
<span data-bind="text: page.pageName" ></span>
<button data-bind="click: book.bookClicked" >Press me</button>
<!-- ko foreach: {data: page.categories, as: 'category'} -->
<span data-bind="text: category.categoryName" ></span>
<button data-bind="click: page.pageClicked" >Press me</button>
<!-- etc -->
<!-- /ko -->
<!-- /ko -->
<!-- /ko -->
None of my declarative bindings directly use $parent.
The problem with the #user2864740 answer is that it doesn't work out of the box jsFiddle.
The first issue:
The binding 'withProperties' cannot be used with virtual elements
To fix that simply add the following code:
ko.virtualElements.allowedBindings.withProperties = true;
After that, you'll get another exception:
Unable to parse bindings. Message: ReferenceError: 'book' is
undefined; Bindings value: foreach: {data: book.pages, as: 'page'}
Which indicates that the withProperties isn't working at all - it hasn't created the book property in the binding context for its child bindings as you might expect.
Below is the fully working and reusable custom binding (jsFiddle):
ko.bindingHandlers.withProperties = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
// Make a modified binding context, with a extra properties, and apply it to descendant elements
var value = ko.utils.unwrapObservable(valueAccessor()),
innerBindingContext = bindingContext.extend(value);
ko.applyBindingsToDescendants(innerBindingContext, element);
// Also tell KO *not* to bind the descendants itself, otherwise they will be bound twice
return { controlsDescendantBindings: true };
}
};
ko.virtualElements.allowedBindings.withProperties = true;
I think this will help you
Calling a function in a parent's scope in nested view model
and the jsfiddle demo
jsfiddle

Categories