Template "data" binding provides value instead of an observable - javascript

I'm trying to use a template binding with its "data" bound to an observable. However, $data for controls inside the template is receiving "value of the observable" (and not the observable itself.)
Since I get the value, and not the observable, I'm unable to set up a 2-way binding if I'm using a template. If I set up a direct binding, the same code works without issue.
Here's a jsfiddle that highlights my problem: http://jsfiddle.net/8cDLw/
HTML
Working Example: <div id="workingExample">
<select data-bind="options: _.range(0,24), hour: MyDate"></select>
Selected Value is: <span data-bind="text: MyDate" />
</div>
<br/>Non-Working Example: <div id="notWorkingExample" data-bind="template: { name: 'hour-template', data: MyDate }"></div>
<script type="text/html" id="hour-template">
<select data-bind="options: _.range(0,24), hour: $data" />
Selected Value is: <span data-bind="text: $data" />
</script>
JavaScript
ko.bindingHandlers.hour = {
init: function(element, valueAccessor, addBindingsAccessor) {
var $el = $(element);
var curDate = valueAccessor();
if (!ko.isObservable(curDate)) {
console.log("Failure: Input Not an observable object. Data type found: " + typeof curDate + ", value: " + curDate);
}
$el.val(ko.utils.unwrapObservable(curDate).getHours());
ko.utils.registerEventHandler(element, "change", function() {
var currentDate = valueAccessor();
var hour = $el.val();
var date = ko.utils.unwrapObservable(currentDate);
date.setHours(hour);
if (ko.isObservable(currentDate))
currentDate(date);
else
console.log("Cannot update value. Input not an observable.");
});
}
};
var viewModel = function()
{
this.MyDate = ko.observable(new Date("2013-11-08T06:27:00.000Z"));
}
ko.applyBindings(new viewModel(), document.getElementById("notWorkingExample"));
ko.applyBindings(new viewModel(), document.getElementById("workingExample"));
Note the 2 divs declared: "workingExample" and "notWorkingExample" are being bound to a new instance of same viewModel (which contains one observable: a hardcoded date.)
workingExample binding is a regular custom binding receiving an observable... in this case, to manipulate "hour" part of a date (which works... i.e. if I change dropdown value, it updates the "hour" in datetime field.)
In NotWorkingExample, I'm passing the observable as "data" to a template (and trying to then use that observable witha binding identical to workingExample.)
I'm clearly missing something here, because NotWorkingExample, from my perspective, should be functionally equivalent to workingExample... the only difference being that instead of directly binding the value, I'm passing the value through template -> "data" binding.
The idea here is to have another <select> in template to have a 2-way "minute" binding as well etc. to essentially build a custom control using the template.
Can you please point out what I'm doing wrong?
EDIT:
Looking at my example, realized that I was unnecessarily complicating things:
Here's a much simpler jsFiddle: http://jsfiddle.net/3kkC5/
Any thoughts on either implementations? Thanks.

when using the data option to a template, KO automatically unwraps it for you and grabs a dependency, so the template updates when the observable data changes.
KO 3.0 does have a new $rawData context variable that is intended to give you the original observable in cases where KO has unwrapped it as part of creating a new context. However, in this case the template binding already unwraps the data option itself. This appears to be a case that we could improve in KO, as ideally $rawData would be the right choice here.
So, in your specific case, you can choose to pass an object literal with your observable as a property like:
template: { name: 'mytmpl', data: { myDate: MyDate } }
Then, you can reference the observable using myDate or whatever you want to call the property.
Here is a sample: http://jsfiddle.net/rniemeyer/a6vtD/
Just as a side note, in your fiddle you had some cases of <span /> and <select />. This can cause issues in your bindings, as they are not self-closing tags in HTML5. You will want to use <span></span> and <select></select>.

Related

Knockout binding handler pass array of objects

Is it possible to pass array value with square brackets for binding handler?, ie:
<div data-bind="validator: [{class: RequiredValidator}, {class: EmailValidator}]"></div>
It works fine for one object:
<div data-bind="validator: {class: RequiredValidator}"></div>
Class value is not observable, just javascript object.
It throws Message: Unexpected token ) error.
Or I need some other syntax? I could wrap it with object, but prefer not.
I took snapshot of project with this issue, available here: http://balin.maslosoft.com/array-validators/dev/validator.php
Open console, and object validators will show configuration, while array will fail.
Here is fiddle with minimal example: http://jsfiddle.net/piotr/fu8d0hm3/
It works for these. Might the problem be in your binding handler?
ko.bindingHandlers.validator = {
init: function(el, va) {
var value = va();
console.debug(value);
}
};
vm = {
something: ko.observable('hi')
};
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="validator: ['one']"></div>
<div data-bind="validator: [something()]"></div>
<div data-bind="validator: [{class: something()}, {class:'whatever'}]"></div>
All you need to set a VALID value to the key you are passing . As in you case RequiredValidator is not defined so keep that in quotes to resolve the issue .
view:
<div data-bind="validator: [{class: 'RequiredValidator'}, {class: 'EmailValidator'}]"></div>
viewModel:
ko.bindingHandlers.validator = {
init: function(element, valueAccessor) {
console.log(valueAccessor()); //check console window for o/p
}
}
ko.applyBindings();
check sample here
It turned out that it was problem with knockout-es5 modification for two-way bindings.
Original plugin is not affected. I've created pull request to address this issue.
The problem was binding preprocessing, which produced invalid code if array value was passed.

Knockout templates and binding to view models

I am having some trouble trying to get knockout templates to work.
I want to use a select list that allows a person to select a value which in turns shows the template.
The template needs to have its own viewmodel properties which are different between each.
I have created a jsfiddle to show the whole thing
I have 2 very basic templates however when I try running the page I get an error. The code is not production codes its simple throw away stuff so naming conventions are not perfect :)
Error: Unable to process binding "foreach: function (){return contacts }" Message: Unable to process binding "template: function (){return { name:contactTypeId} }" Message: Unknown template type: 1
The template do exist
<script type="text/html" id="1">
<span> Family Template </span>
<input placeholder="From Mum or Dads side"/>
</script>
<script type="text/html" id="2">
<span> Friend Template </span>
<input placeholder="Where did you meet your friend"/>
</script>
I am trying to select the template via a select
<select class="form-control" data-bind="options: $root.contactTypes,
optionsText: 'type',
optionsValue:'id',
value:contactTypeId,
optionsCaption: 'Please Select...'"></select>
2 questions.
Why can it not find the template when I select it from the dropdown?
How would I bind the template to have its own model to allow me to save properties.
Update
Thanks to Georges answer below I have the template binding working. Turns out you can't use an int as an ID for a template without calling to
I have updated my model
self.contactTypeTemplateModel = ko.computed(function () {
return self.contactTypeId === 2 ? someModelWithWhereDidYouMeet : someOtherModel
});
var someModelWithWhereDidYouMeet = {something:ko.observable()};
var someOtherModel = {something:ko.observable()};
It maybe due to no sleep but I can't get this to work. The console is telling me "something is not defined"
Granted my naming is not good. I have also updated the fiddle
The problem for question #1 seems to be that you're passing in a number where it expects a string. For whatever reason, it's not being automatically coerced. This solves it.
template: { name: contactTypeId().toString() }
Even better, create a computed and add a reasonable prefix.
templateName = ko.computed(function() { return "contact-type-" + contactTypeId() })
As for passing in different models. The template binding supports the data property. Your data property can be a computed based on contactTypeId as well.
So you do your template binding with
template: {name: contactTypeTemplateName(), data: contactTypeTemplateModel() }
Where
self.contactTypeTemplateModel = ko.computed(function() {
return self.contactTypeId() === 2 ? someModelWithWhereDidYouMeet
: someOtherModel })
I should also mention, that unless you reuse these templates independently from each other in lots of places, I wouldn't recommend templates for this. I would just use an if binding.

Copying knockout object to another while updating the layout

When my user select one of the object in my observable array, I want to copy it to a "selectedObject". But when I do that, the layout binding on the "SelectedObject" are not updated.
So I've created an update method but I find it very difficult to maintain. is there a better way?
Here is my selected object ui:
<div class="row" data-bind="with: SelectedFlightObject">
<div>select object:</div>
<div data-bind="html: FlightNumber"></div>
</div>
Here is the js that I want to work but doesn't:
//this do not update the layout:
this.OnFlightClick = function (selectObject) {
this.SelectedFlightObject = selectObject;
}.bind(this);
Here is the js that update the ui but find it hard to maintain.
UpdateFlightObject: function (currentObj, newObj) {
currentObj.AirplaneType(newObj.AirplaneType());
currentObj.ArrivingDate(moment(newObj.ArrivingDate()));
currentObj.FlightNumber(newObj.FlightNumber());
currentObj.Duration(newObj.Duration());
currentObj.ArrivalCode(newObj.ArrivalCode());
currentObj.DeparturCode(newObj.DeparturCode());
},
this.OnFlightClick = function (selectObject) {
FlightFactory.UpdateFlightObject(this.SelectedFlightObject, selectObject);
}.bind(this);
Knockout requires you to use their observable wrappers. These wrappers are where the magic happens, once bound, they are what reports changes in values and receive user input back. Your code should look something like this.
Create:
this.SelectedFlightObject = ko.observable(someInitialValueOrNull);
Retrieve:
this.SelectedFlightObject();
Update:
this.SelectedFlightObject(someNewValueOrNull);
You should make your SelectedFlightObject observable so that when it changes your layout is updated.
this.OnFlightClick = function (selectObject) {
this.SelectedFlightObject(selectObject);
}.bind(this);
Your initial SelectedFlightObject would of course need to have some initial values for flight number, etc...
Remember that doing this does not create a new object

Knockout change event-handler

I am spending hours trying to get a simple event call working correctly in my durandal/knockout app.
Context
I have a list of languages that that the user can select from a select-box:
<select class="form-control select2"
data-bind="event: { change: app.languageChanged }, options:languages,
optionsText:'label',
optionsValue:'code',
value:app.selectedLanguage"></select>
The property app.selectedLanguage is a ko.observable. I know that this works because the correct item gets pre-selected.
this.selectedLanguage = ko.observable(options.defaultLanguage);
I also have an event-handler which listens for changes on that select box, so that I can send a message to other parts of the application that need to be informed:
languageChanged : function(data, event) {
console.log(data);
console.log(event);
console.log(this.selectedLanguage());
app.trigger('language:change', this.selectedLanguage());
},
The problem
the first parameter 'data' does not contain the selected item, but instead contains all items (actually, it seems to be the complete current view-model).
If 1. does not work, then it would be an alternative to at least get hold of the new value from the observable 'selectedLanguage'. Unfortunately that always seems to have the old value. So whenever I change the selectbox option, I always get the previously selected value.
Question
So the question is: what could I be doing wrong? I am sure that this normally works correctly and I must be missing something somewhere.
I thought I had finally understood how knockout works, but now I have come across the next issue. I would be very grateful if someone could help me on this.
EDIT [SOLVED]
Thanks to xdumaine, here is the (nice and simple) solution:
In my html template, I removed the change-event:
<select class="form-control select2"
data-bind="options:languages,
optionsText:'label',
optionsValue:'code',
value:app.selectedLanguage"></select>
In my App view-model (that I require everywhere), I now subscribe to the ko.observable instead of listening to the event-handler:
define([ 'durandal/app', 'underscore', 'knockout', 'myapp/myapp' ], function(app, _, ko, myapp) {
"use strict";
function App(options) {
if (!(this instanceof App)) {
throw new TypeError("App constructor cannot be called as a function.");
}
this.options = options || {};
// Set the initial language.
this.selectedLanguage = ko.observable(options.defaultLanguage);
// *** Subscribes to the observable ***
this.selectedLanguage.subscribe(function(newValue) {
console.log(newValue);
app.trigger('language:change', newValue);
});
_.bindAll(this, 'getSelectedLanguage');
}
App.prototype = {
constructor : App,
getSelectedLanguage : function() {
return this.selectedLanguage();
}
}
return App;
});
This code has therefore been removed and is no longer needed:
languageChanged : function(data, event) {
console.log(data);
console.log(event);
console.log(this.selectedLanguage());
app.trigger('language:change', this.selectedLanguage());
},
Best regards,
Michael
Why bind to the select change event instead of just subscribing to the selectedLanguage?
var self = this;
self.selectedLanguage = ko.observable();
self.selectedLangauge.subscribe(function(newValue) {
console.log(newValue);
app.trigger('language:change', newValue);
});
If you want to do it like you have it, know this: event bindings in knockout always get a reference to the viewModel as the first parameter, and the event data as the second, so you could would have to inspect the event to get the target and extract the value if you're doing it that way. The reason 2 is not working is that your change event is firing before the knockout observable is notified, so you get timing issues. This could have different behavior in different browsers.
I'd recommend sticking to observable subscriptions, instead of using DOM events, whenever possible.

Knockout templates: afterRender triggered twice because name/data are coupled observables

I'm having trouble getting afterRender to trigger exactly once. I'm seeing that afterRender is triggered twice because I have two observables in my template binding that update at the same time:
<div data-bind="template: { name: template.name(), data: template.data, if: template.data, afterRender: doSomeStuff }">
</div>
This is because an update to name triggers an update to data since it is a computed observable whose value depends on name:
this.data = ko.computed({
var viewName = this.name();
switch (viewName) {
case "a":
return this.modelA();
case "b":
return this.modelB();
case "c":
return this.modelC();
default:
return null;
}
});
What I'm trying to do is log the time the template is rendered. I'm also concerned about potential performance implications of rendering twice - are those DOM elements within the template being created twice every single time name changes?
It is possible for data to change (e.g. the modelA, modelB or modelC object changes) without name changing and I do want data changes to trigger afterRender if it is changed by itself. Ideally, I would be able to couple the name-data update to trigger afterRender once but I'm not sure if this is possible.
Is there an elegant way to accomplish this?
I looked into throttle extenders but those wouldn't prevent the
template binding from being triggered twice
deferEvaluation on the
data computed observable does not appear to cause the template
binding to pick up the new object when template.name changes (not
sure if this is because the template binding is not actually
reevaluating data when template.name changes), and should still trigger the template binding twice.
I'm thinking about getting rid of the data object completely and in each HTML template, attaching a with binding directly to modelA, modelB, and modelC, but then I wouldn't get those afterRender updates if the data changes.
Edit: I am using KO 2.2.1, and realized that the template name can't be observable (until 2.3.0), hence the parentheses after template.name. So my original assumption that this afterRender is happening twice because name is an observable too must be incorrect?
Bindings are updated using a computed observable that tracks changes to any dependencies. Your situation is like this: computed C (template binding) is dependent on computed B (data) and observable A (name), and computed B is dependent on observable A. When A changes, it updates B and C. And when B updates, it also updates C. So that's why C gets two updates.
For reference, here's your example in jsFiddle showing two updates: http://jsfiddle.net/mbest/DnY9V/
Throttle extender
Knockout's throttle extender is the only built-in way to prevent multiple updates. You can create a custom binding that wraps the template binding to run the update in a throttled computed observable.
ko.bindingHandlers.throttledTemplate = {
init: function(element) {
var args = arguments, self = this,
result = ko.bindingHandlers.template.init.apply(this, arguments);
ko.computed(function() {
ko.bindingHandlers.template.update.apply(self, args);
}, null, {disposeWhenNodeIsRemoved: element}).extend({throttle:1});
return result;
}
};
http://jsfiddle.net/mbest/j5D6p/
Deferred Updates plugin
Alternatively, you can use my Deferred Updates plugin that prevents all duplicate updates to computed observables: https://github.com/mbest/knockout-deferred-updates
http://jsfiddle.net/mbest/UXYJx/

Categories