Knockout Prototype Issue - javascript

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.

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 -->

How to bind more than 1 property to element?

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!

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.

Knockoutjs remove multiple by selecting checkbox

I have a list of "assets" that I am displaying using foreach bind.
Each asset has a delete button which calls $parent.removeAsset, which all works fine.
However, I want to add the option to select several "assets" by checking a checkbox, and then remove all the "assets" that are checked.
I am still learning the ropes of knockoutjs so I would really appreciate any help.
Here is the code I am using to display the "assets
<div style="height: 100%; overflow: auto;" data-bind="foreach: assets">
<!-- AssetList AssetItem Tmpl BEGIN -->
<div class="asset-item action" data-tooltip="Select Asset">
<div class="asset-type" data-bind="css: type"> </div>
<div class="asset-select"><input type="checkbox" /></div>
<!-- ko if: type() === 'Text' -->
<div class="asset-name" data-bind="text: content"></div>
<!-- /ko -->
<!-- ko if: type() === 'Image' -->
<div class="asset-name">Image</div>
<!-- /ko -->
<!-- ko if: type() === 'Video' -->
<div class="asset-name">Video</div>
<!-- /ko -->
<div class="asset-remove-cell">
<div class="asset-remove action" data-tooltip="Remove Asset" data-bind="click: $parent.removeAsset"></div>
</div>
</div>
<!-- AssetList AssetItem Tmpl END -->
</div>
And this is my delete function:
self.removeAsset = function(asset){
if (!confirm("Are you sure you want to delete this asset?")) {
event.stopImmediatePropagation();
return false;
}
self.selectedIndex(0);
$.ajax({
url: "/Assets/delete/"+asset.id(),
type: "POST",
success: function(response) {
self.assets.remove(asset);
//notify('good',response);
}
});
};
As #Kenneth suggested I added a boolean to my assets and then looped through the observable array and deleted each asset that was set to true. Here is the code I used for anyone looking to do something similar:
The array:
function Asset() {
var self = this;
self.id = ko.observable("");
self.type = ko.observable("");
self.selected = ko.observable(false);
};
The delete checkbox:
<input type="checkbox" data-bind="checked: selected" />
The function called when the delete button is pressed:
$('#deleteMultipleAssets').click(function(){
if (confirm('Are you sure you want to delete the selected asset?')) {
ko.utils.arrayForEach(viewModel.assets(), function(asset) {
if(asset.selected()){
viewModel.removeMultipleAsset(asset);
}
});
}
});
The removeMultipleAssets function:
self.removeMultipleAsset = function(asset){
self.assets.remove(asset);
};
You should add a field to your assets called selected and then bind that field to the checkbox you're displaying inside the foreach-template.
When you click on the general delete-button, you can loop over your assets and delete all the ones that have their selected-field equal to true and remove them.

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