I am trying to fill my view model with some drop down options from the database. I want to keep track of the selected object because it has properties on it that I am using in a custom binding elsewhere.
I initialize the observable with a "blank" value so that it's properly set when the binding occurs and my custom binding works. Once the server responds, I morph the data to an observable array and the drop down list displays the options.
Here is the JavaScript code:
ko.bindingHandlers.jq_fade = {
init: function (element, valueAccessor) {
// Initially set the element to be instantly visible/hidden depending on the value
var value = valueAccessor();
$(element).toggle(ko.utils.unwrapObservable(value)); // Use "unwrapObservable" so we can handle values that may or may not be observable
},
update: function (element, valueAccessor) {
// Whenever the value subsequently changes, slowly fade the element in or out
var value = valueAccessor();
ko.utils.unwrapObservable(value) ? $(element).fadeIn() : $(element).fadeOut();
}
};
function Firm(id, name, outsideCounsel) {
this.name = name;
this.id = id;
this.outsideCounsel = outsideCounsel;
}
function RequestViewModel() {
var self = this,
ONE_DAY = 1000 * 60 * 60 * 24;
self.firm = ko.observable(new Firm(-1, "", false));
$.post(ajaxAddress + 'LoadFirms', function (data) {
var mappedFirms = $.map(data.d, function (item) {
return new Firm(item.OrganizationLookupID, item.OrganizationLookupName, item.IsExternalCounselFirm);
});
self.availableFirms(mappedFirms);
self.firm(self.availableFirms()[0]);
});
}
$(document).ready(function () {
model = new RequestViewModel();
ko.applyBindings(model);
});
Here is the relevant HTML
<span data-bind="jq_fade: firm().outsideCounsel">
Outside Counsel
<input type="text" id="outsideCounsel" data-bind="value: outsideCounsel" />
</span>
I want that DIV to show only if the selected firm is an outside counsel. If remove the line data-bind="jq_fade: firm().outsideCounsel, everything works fine. If I make the $.post() calls synchronously, it works. I'm thinking it's something wrong with my init function in jq_fade.
The error I receive is:
Uncaught Error: Unable to parse bindings.
Message: TypeError: Cannot call method 'outsideCounsel' of undefined;
Bindings value: jq_fade: firm().outsideCounsel()
I understand what Knockout is telling me, I'm just not sure how firm() can ever be undefined because I set up an initial value.
If you're binding availableFirms() to a dropdownlist, I'm assuming you've also bound firm() to the same list so that when another is selected from the list, firm() gets automatically updated and all your bindings update automatically.
If this is the case, you do not need to set firm() at all as it will be set to the first element in the dropdownlist anyway.
See example 3 here:
http://knockoutjs.com/documentation/options-binding.html
var viewModel = {
availableCountries : ko.observableArray([
new country("UK", 65000000),
new country("USA", 320000000),
new country("Sweden", 29000000)
]),
selectedCountry : ko.observable() // Nothing selected by default
};
Try it like above without specifically setting firm() and see if it errors again
Related
I am currently using an event binding to format telephone numbers (into xxx-xxx-xxxx format) and I want to create a reusable custom binding to this for future use in our app. The current event binding works perfectly but I cannot get the custom binding to work correctly. Can anyone take a look below and tell me my issue?
Current event binding with viewModel method:
<input class="form-control" id="Phone" type="text"
data-bind="event: {blur: formatPhone}, enable: isInputMode, value: Phone" />
self.Phone = ko.observable(model.MainPhone).extend({ maxLength: 20 });
self.formatMainPhone = function() {
var tempString = self.Phone().replace(/\D+/g, "").replace(/^[01]/, "").replace(/(\d{3})(\d{3})(\d{4})/, "$1-$2-$3").substring(0, 12);
self.Phone(tempString);
}
Custom binding handler that does not work:
<input class="form-control max225" id="Phone" type="text"
data-bind="formatPhoneNumber: Phone, enable: isInputMode, value: Phone" />
self.Phone = ko.observable(model.MainPhone).extend({ maxLength: 20 });
ko.bindingHandlers.formatPhoneNumber = {
update: function (element, valueAccessor) {
var phone = ko.utils.unwrapObservable(valueAccessor());
var formatPhone = function () {
return phone.replace(/\D+/g, "").replace(/^[01]/, "").replace(/(\d{3})(\d{3})(\d{4})/, "$1-$2-$3").substring(0, 11);
}
ko.bindingHandlers.value.update(element, formatPhone);
}
};
Your binding is attempting to hijack the update of the default "value" binding, which looking at the knockout source code, appears to have been deprecated.
'update': function() {} // Keep for backwards compatibility with code that may have wrapped value binding
You'll have to change your binding so that it uses init instead.
ko.bindingHandlers.value.init(element, formatPhone, allBindings);
EDIT:
This might be closer to what you want. Instead of using the update binding this creates an intermediary computed observable and then uses value.init to bind the textbox to that. We never need the update binding because the computed will take care of propagating changes for you.
ko.bindingHandlers.formatPhoneNumber = {
init: function (element, valueAccessor, allBindings) {
var source = valueAccessor();
var formatter = function(){
return ko.computed({
read: function(){ return source(); },
write: function(newValue){
source(newValue.replace(/\D+/g, "").replace(/^[01]/, "").replace(/(\d{3})(\d{3})(\d{4})/, "$1-$2-$3").substring(0, 12));
}
})
};
ko.bindingHandlers.value.init(element, formatter, allBindings);
}
};
EDIT 2 - more explanation
Using the update binding for formatPhoneNumber tells knockout to run that code any time the value changes. That sounds good in theory, but lets break it down.
Unwraps the accessor and gets the flat value.
Creates a format function that returns a formatted value.
piggyback's the value binding using the format function.
Because this is an update binding unwrapping the accessor in step 1 creates a trigger to re-evaluate the binding whenever the accessor value changes. Then in step 3 you're telling knockout to re-execute the value.update binding which currently is just an empty function and does nothing. If you change that to use value.init instead things might actually function for formatting the output, but you're telling knockout to re-initialize the init binding every time the value changes.
update: function(element, valueAccessor, allBindings) {
var phone = ko.utils.unwrapObservable(valueAccessor());
var formatPhone = function() { return phone.replace(...)}
ko.bindingHandlers.value.init(element, formatPhone, allBindings);
}
The binding is getting re-created and passed new initial values. This also means that it's only a one-way binding because changes to the front-end can't make it back to your model to update the backing observable.
Now if you change your own binding to an init binding, and from there call the value.init binding as well it will only get initialized the one time, but the next problem is that the function you're binding to isn't going to know when to update.
init: function(element, valueAccessor, allBindings) {
var phone = ko.utils.unwrapObservable(valueAccessor());
var formatPhone = function() { return phone.replace(...)}
ko.bindingHandlers.value.init(element, formatPhone, allBindings);
}
Since it's just a normal js function which is being passed an already-unwrapped flat value it will always always give the same result based on that original value of phone. Passing the value.init binding a computed observable instead ensures that updates to the accessor observable trigger the format function to update from within the existing binding.
Hopefully this will be a quick one for a knockout guru....
I'm writing a couple of custom bindings to help me translate a UI using a custom translation engine in the project I'm working on.
One is to translate text, the other is to translate the 'placeholder' attribute on HTML5 input elements.
Both bindings are identical apart from the last statement, where one updates the text in the element and the other updates an attribute value.
The text one works perfectly, but the place holder one does not, and I'm stuck for an answer as to why.
The binding code is as follows:
Translated Text Binding
ko.bindingHandlers.translatedText = {
init: (element: HTMLElement, valueAccessor: Function, allBindings: KnockoutAllBindingsAccessor, viewModel: any, bindingContext: KnockoutBindingContext) => {
// Get our custom binding values
var value = valueAccessor();
var associatedObservable = value.observable;
var translationToken = value.translationToken;
// Set up an event handler that will respond to events telling it when our translations have finished loading
// the custom binding will instantly update when a key matching it's translation ID is loaded into the
// local session store
window.addEventListener("TranslationsLoaded", (e) => {
//associatedObservable(" "); // Force an update on our observable, so that the update routine below is triggered
associatedObservable.valueHasMutated();
}, false);
},
update: (element: HTMLElement, valueAccessor: Function, allBindings: KnockoutAllBindingsAccessor, viewModel: any, bindingContext: KnockoutBindingContext) => {
// Get our custom binding values
var value = valueAccessor();
var associatedObservable = value.observable;
var translationToken = value.translationToken;
// Ask local storage if we have a token by that name
var translatedText = utilityLib.getTranslatedString(translationToken);
// Check if our translated text is defined, if it's not then substitute it for a fixed string that will
// be seen in the UI (Whatever you put into the 'associatedObservable' at this point WILL appear in the element
if (undefined === translatedText || translatedText === "" || translatedText === null) {
if (sessionStorage["translations"] === undefined) {
// No translations have loaded yet, so we blank the text
translatedText = "";
} else {
// Translations have loaded, and the token is still not found
translatedText = "No Translation ID";
}
}
associatedObservable(translatedText);
ko.utils.setTextContent(element, associatedObservable());
}
} // End of translatedText binding
Translated Placeholder Binding
ko.bindingHandlers.translatedPlaceholder = {
// This one works pretty much the same way as the translated text binding, except for the final part where
// the translated text is inserted into the element.
init: (element: HTMLElement, valueAccessor: Function, allBindings: KnockoutAllBindingsAccessor, viewModel: any, bindingContext: KnockoutBindingContext) => {
var value = valueAccessor();
var associatedObservable = value.observable;
var translationToken = value.translationToken;
window.addEventListener("TranslationsLoaded", (e) => {
debugger;
associatedObservable.valueHasMutated();
}, false);
},
update: (element: HTMLElement, valueAccessor: Function, allBindings: KnockoutAllBindingsAccessor, viewModel: any, bindingContext: KnockoutBindingContext) => {
var value = valueAccessor();
var associatedObservable = value.observable;
var translationToken = value.translationToken;
var translatedText = utilityLib.getTranslatedString(translationToken);
debugger;
if (undefined === translatedText || translatedText === "" || translatedText === null) {
if (sessionStorage["translations"] === undefined) {
translatedText = "";
} else {
translatedText = "No Translation ID";
}
}
associatedObservable(translatedText);
element.setAttribute("placeholder", translatedText);
}
} // End of translatedPlaceholder binding
The idea is a simple one, if the binding runs and the translations are already present in sessionStorage, then we pick up the translated string and plug it in to the observable associated with the element.
If the translations have loaded, but the translation is not found "No Translation ID" is plugged into the observable bound to the element.
If the translations have NOT yet loaded, plug an empty string into the observable, then wait for the event 'TranslationsLoaded' to fire. When this event is raised, the observable bound to the element is mutated, causing an update to happen, which in turn re-checks the translations, which it then finds have loaded and so acts accordingly.
However.....
It doesn't matter how hard I try, the translated placeholder binding just will not fire it's update.
I can clearly see in the debugger that the event is recieved on both bindings, and the mutate function IS called.
On the translated text binding, I get the following sequence...
'init' -> 'update' -> 'event' -> 'mutate' -> 'update'
Which is exactly what I expect, and it occurs on every element+observable bound to that binding.
On the translated placeholder i get
'init' -> 'update' -> 'event' -> 'mutate'
but the final update never occurs.
As a result, the translated string for the placeholder is never looked up correctly, the text one with identical code works perfectly!!
For those who'll ask, i'm using the bindings like this:
<input type="text" class="form-control" data-bind="value: userName, translatedPlaceholder: { observable: namePlaceHolderText, translationToken: 'loginBoxNamePlaceholderText'}">
<span class="help-block" data-bind="translatedText: {observable: nameErrorText, translationToken: 'loginBoxUserNameEmptyValidationText'}"></span>
and inside the view model, the 'observable' parameters are just normal ko.observable variables holding strings.
Cheers
Shawty
I believe you have event bubbling issues... try putting a 'return true' after your call to valueHasMutated like this:
init: (element: HTMLElement, valueAccessor: Function, allBindings: KnockoutAllBindingsAccessor, viewModel: any, bindingContext: KnockoutBindingContext) => {
var value = valueAccessor();
var associatedObservable = value.observable;
var translationToken = value.translationToken;
window.addEventListener("TranslationsLoaded", (e) => {
associatedObservable.valueHasMutated();
return true; // allow event to bubble
}, false);
},
That being said, I am thinking all this manual eventing is against-the-grain for what knockout can do for you. You should be observing your data, binding to it, and letting knockout do all the internal eventing for you... that's what it does.
An example building on user3297291's fiddle:
ko.bindingHandlers.translatedPlaceholder = {
init: function(element, valueAccessor) {
var va = valueAccessor();
var obs = ko.utils.unwrapObservable(va.obs);
var placeholderStr = obs[va.key];
console.log(placeholderStr);
element.setAttribute("placeholder", placeholderStr);
},
update: function(element, valueAccessor) {
var va = valueAccessor();
var obs = ko.utils.unwrapObservable(va.obs);
var placeholderStr = obs[va.key];
console.log(placeholderStr);
element.setAttribute("placeholder", placeholderStr);
}
};
var vm = function() {
var self = this;
self.dictionary = ko.observable({
"placeholder": "Initial State"
});
self.switchTranslations = function() {
// Set the 'new' dictionary data:
self.dictionary({
"placeholder": "My Translated Placeholder"
});
};
}
ko.applyBindings(new vm());
Fiddle: https://jsfiddle.net/brettwgreen/5pmmd0va/
Trigger the update handler
From the Knockout documentation:
Knockout will call the update callback initially when the binding is applied to an element and track any dependencies (observables/computeds) that you access. When any of these dependencies change, the update callback will be called once again.
The key is that you have to access the observable within the update function to get updates. In your translatedText binding you do so:
ko.utils.setTextContent(element, associatedObservable());
But there is no such access of associatedObservable in the update function of translatedPlaceholder. You'll need to add it like this:
associatedObservable(translatedText);
associatedObservable(); // get notified of updates to associatedObservable
element.setAttribute("placeholder", translatedText);
A better approach
For your case, there really isn't a need for an update handler because you don't need to update the view based on changes to the viewmodel. Instead your updates just come from events, which can be set up in the init handler.
ko.bindingHandlers.translatedPlaceholder = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
function loadTranslation() {
var translationToken = valueAccessor(),
translatedText = utilityLib.getTranslatedString(translationToken);
element.setAttribute("placeholder", translatedText || "No Translation ID");
window.removeEventListener("TranslationsLoaded", loadTranslation);
}
if (sessionStorage["translations"] === undefined)
window.addEventListener("TranslationsLoaded", loadTranslation, false);
} else {
loadTranslation();
}
}
}
Usage:
data-bind="value: userName, translatedPlaceholder: 'loginBoxNamePlaceholderText'"
Can someone tell me why this works?
<div id="test">
<label data-bind="dateText: TheDate"></label>
</div>
ko.bindingHandlers.dateText = {
update:
function(el, f_valueaccessor, allbindings, viewmodel, bindingcontext)
{
// Get current value of supplied value
var val = ko.unwrap(f_valueaccessor());
console.log('dt is ' + (typeof(val.dt) == 'function' ? '' : 'NOT ') + 'observable');
var dt = ko.unwrap(val.dt);
$(el).html(dt ? moment(dt).format('DD/MM/YYYY') : '');
}
};
function Model(data, mapping)
{
var _this = this;
if(data)
ko.mapping.fromJS(data, mapping, _this);
this.TheDate = this.TheDate || { dt: ko.observable() }
}
var model = new Model({ TheDate: { dt: null } });
ko.applyBindings(model);
model.TheDate.dt('2001-01-01T01:01:01');
Fiddle: http://jsfiddle.net/qkLzc5u5/2
The reason I ask is that I am having problems making this custom binding work in a real application - there, it displays any initial date fine but updates are not reflected in the text - however the IsDated computed does update, so the label is displayed; just blank.
I thought that the problem was that the binding is attached to TheDate, which is not an observable itself; only dt is.
However, the code above will in fact fire the update, so the date 01/01/2001 is shown, whereas I was expecting nothing to be displayed due to the null initial value.
Now this is actually exactly the behaviour I want, so I don't have a problem with this code - I am hoping to get a bit more of an insight into how the custom bindings know when to fire so I can debug my real world code better.
(Note that the object model for passing dates is fixed by a web service that I cannot change)
I'm writing a Knockout.js application which performs basic CRUD operations on a graph of entities. I load two JSON-encoded objects from my server: the first is an array containing a bunch of product details, the second is a list of indexed images as follows:
function UploadedImage(data) {
this.UploadedImageId = data.UploadedImageId;
this.Url = data.Url;
this.Name = data.Name;
this.AltText = data.AltText;
}
...which is stored in the ViewModel via an AJAX request as follows:
$.post("#Url.Action("JsonRepairData")", {}, function (response) {
self.repairData(_.map(response.repairData, function(d) { return new AppleProduct(d); }));
self.images(_.map(response.images, function (i) { return new UploadedImage(i); }));
self.dataLoaded(true);
}, 'json');
I've verified that the data is all loaded correctly, both in the object graph and in the image array. The image IDs in the object graph correspond to the image IDs in the array. To display what image is selected for relevant data entries in the object graph, I opted for a select list as follows:
<tbody data-bind="foreach: repairData">
<tr>
<td>#Html.TextBoxFor(m => m.Name, new { data_bind = "value: Name" })</td>
<td><select data-bind="options: $root.images, optionsCaption: 'Pick an image...', optionsValue: 'UploadedImageId', optionsText: 'Name', value: UploadedImageId"></select></td>
<td data-bind="html: ImgTag"></td>
<td>delete</td>
</tr>
</tbody>
Note that repairData, which is contained in the ViewModel, is the aforementioned object graph. All other data binds correctly. The select box successfully populates. The value of UploadedImageId as bound in the ViewModel updates as the select box is manipulated. However, the ViewModel's initial value of UploadedImageId (from the object graph) is disregarded and when the page renders, UploadImageId is immediately updated to undefined. This is how the UploadedImageId's initial value is set in the object graph:
function AppleProduct(data) {
var productSelf = this;
this.Id = data.Id;
this.Name = ko.observable(data.Name);
this.UploadedImageId = ko.observable(data.UploadedImageId);
this.Variations = ko.observableArray(_.map(data.Variations, function (v) { return new ProductSeries(v); }));
this.ImgTag = ko.computed(function () {
var img = _.find(self.images(), function (i) { return i.UploadedImageId == productSelf.UploadedImageId(); });
return img ? '<img src="'+img.Url+'" />' : '';
});
// Remove computed ImgTag from data addressed to the server
this.toJSON = function () {
var copy = ko.toJS(this);
delete copy.ImgTag;
return copy;
};
}
I've been trying to figure out how to get the select box to set correctly for hours and I'm still completely stumped. All I can determine is that in the 2.1.0 knockout js source file the code responsible for setting the initial value of the select box on line 1402 (see below) is passed twice. The first time the model values properly correspond to the image IDs, but element.options.length == 0 and so the model value isn't used. The second time this code is passed, the model values are all undefined but the select box is populated (and so element.options.length is a positive number), and so again the proper initial value is not set.
for (var i = element.options.length - 1; i >= 0; i--) {
if (ko.selectExtensions.readValue(element.options[i]) == value) {
element.selectedIndex = i;
break;
}
}
Any help in understanding what I'm missing would be greatly appreciated!
As nemesv has cleverly spotted, the problem was the ordering of the population of the ViewModel data:
$.post("#Url.Action("JsonRepairData")", {}, function (response) {
self.repairData(_.map(response.repairData, function(d) { return new AppleProduct(d); }));
self.images(_.map(response.images, function (i) { return new UploadedImage(i); }));
self.dataLoaded(true);
}, 'json');
Due to the aggressiveness of Knockout.js dependency tracking, the system attempts to populate the object graph views before it even has access to image data. That's why when it tries to create select boxes, the select boxes have no data to populate them and their default values can't be set. By switching the order in which repairData and images ViewModel data load, image data becomes available when it's needed and the application runs.
This seems to demonstrate that Knockout.js dependency tracking via ko.observables, whereas a powerful expressive tool, can be dangerous if proper care is not taken with unintended logic resulting from ViewModel state mutation.
Story:
I have a lot of properties that needs to be set. Since they have similarities I choose to read out the class the inputbox is part of as property name.
Problem:
A dynamic property of an object can be set, just like an associative array. So I could do
var customattribute = this.$el.parents('div').attr('id');
var value = $(e.currentTarget()).val();
this.model.attributes[customattribute] = value;
But this wouldn't trigger a change event of the model. I could manually trigger a change event, but this wouldn't update this.model.changedAttributes(), I need to set only the changed attribute, not every attribute.
This of course doesn't work either:
this.model.set(customattribute: value);
So how would I handle this problem?
I have ALOT (200+) attributes that can be set, I wouldn't like to make separate eventlisteners for every attribute unless thats the only way.
Code:
var Display = Backbone.View.extend({
className: 'display',
events: {
'slide .slider' : 'sliderHandler'
},
initialize: function(){
_.bindAll(this, 'render', 'sliderHandler','update');
this.model.on('change',this.update, this);
},
render: function(){
this.$el.html(_.template(html, {}));
this.$('.slider').slider();
return this;
},
sliderHandler: function(e){
var slider = $(e.currentTarget);
var property = slider.parents('div').attr('id');
var value = slider.slider('value');
this.model.attributes[property] = value;
},
update: function(){
console.log(this.model.changedAttributes());
//get changed attribute + value here
},
});
Edit:
The two answers below solved it. Map the attributes to an object and give that to Backbone. Also I found another solution. Instead of an object, model.set() also accepts an array.
model.set(customattribute, value, customattribute2, value2);
I'm not sure that I fully understand your problem, but:
If you want to update an attribute, just:
this.model.set({
customattribute: value
});
If you want that the setting not trigger an event, you could pass a silent option, like this:
this.model.set({
customattribute: value
}, {silent:true});
I hope it helps you.
UPDATED:
Another way:
var map = {};
map[customattribute] = value;
this.model.set(map);
You need to do this
var attributeName = this.$el.parents('div').attr('id');
var value = $(e.currentTarget()).val();
var attribute = {};
attribute[attributeName] = value;
this.model.set(attribute);
if attribute was "test" and value 'value' it would be the same as
this.model.set({test: 'value'});
which is what correctly sets the attribute and propagates to your view thanks to model change event
As of Backbone 0.9.0 (Jan. 30, 2012), model.set (source) accepts a key, a value, and an options object.
this.model.set(attributeName, value, { silent: true });
is equivalent to
var attr = {};
attr[attributeName] = value;
this.model.set(attr, { silent: true });