This seems to be a common approach to sanitizing/validating/formatting data with knockout when binding to an input field, it creates a reusable custom binding that uses a computed observable. It basically extends the default value binding to include an interceptor that will format/sanitize/validate input before written/read.
ko.bindingHandlers.amountValue = {
init: function (element, valueAccessor, allBindingsAccessor) {
var underlyingObservable = valueAccessor();
var interceptor = ko.computed({
read: function () {
// this function does get called, but it's return value is not used as the value of the textbox.
// the raw value from the underlyingObservable is still used, no dollar sign added. It seems like
// this read function is completely useless, and isn't used at all
return "$" + underlyingObservable();
},
write: function (newValue) {
var current = underlyingObservable(),
valueToWrite = Math.round(parseFloat(newValue.replace("$", "")) * 100) / 100;
if (valueToWrite !== current) {
// for some reason, if a user enters 20.00000 for example, the value written to the observable
// is 20, but the original value they entered (20.00000) is still shown in the text box.
underlyingObservable(valueToWrite);
} else {
if (newValue !== current.toString())
underlyingObservable.valueHasMutated();
}
}
});
ko.bindingHandlers.value.init(element, function () { return interceptor }, allBindingsAccessor);
},
update: ko.bindingHandlers.value.update
};
jsFiddle example: http://jsfiddle.net/6wxb5/1/
Am i missing something? I've seen this method used everywhere, but it doesn't seem to fully work. The read function seems completely useless as it doesn't get used at all.., and in the write function, entering "23.0000" changes the written value to 23, but the textbox values do not update.
The issue comes from the update portion of your custom binding. This part will update the field based on the original model value. So, the event handler attached in the init will send the new value through your writeable computed, but the updating of the field actually happens in the update.
One option is to apply the value binding from your init function and skip the update function like:
ko.bindingHandlers.amountValue = {
init: function (element, valueAccessor, allBindingsAccessor) {
var underlyingObservable = valueAccessor();
var interceptor = ko.computed({
read: function () {
// this function does get called, but it's return value is not used as the value of the textbox.
// the raw value from the underlyingObservable, or the actual value the user entered is used instead, no
// dollar sign added. It seems like this read function is completely useless, and isn't used at all
return "$" + underlyingObservable();
},
write: function (newValue) {
var current = underlyingObservable(),
valueToWrite = Math.round(parseFloat(newValue.replace("$", "")) * 100) / 100;
if (valueToWrite !== current) {
// for some reason, if a user enters 20.00000 for example, the value written to the observable
// is 20, but the original value they entered (20.00000) is still shown in the text box.
underlyingObservable(valueToWrite);
} else {
if (newValue !== current.toString())
underlyingObservable.valueHasMutated();
}
}
});
ko.applyBindingsToNode(element, { value: interceptor });
}
};
Updated fiddle: http://jsfiddle.net/rniemeyer/Sr8Ev/
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'"
I'm building a very number-heavy app in KnockoutJS and I want to have the ability to format large numbers so that they're comma-seperated and nice on the eye (xxx,xxx).
As you'll see from the fiddle below, I do have this working by wrapping the binded value inside of a formatting function with a simple RegEx but the problem with this is that this overwrites the value inside the input and inserts ',' into the underlying value.
The large numbers are used further down the app and so to prevent NaN errors I've had to assign a data attribute to the input value containing the value with no ',' and this is the value that gets stored in sessionStorage.
I feel that I have unncessarily bloated my HTML markup and believe that what I want to achieve is possible with a bindingHandler but my binding handler isn't quite there.
Fiddle: http://jsfiddle.net/36sD9/2
formatLargeNumber = function (number) {
if (typeof (number) === 'function') {
return number().toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
}
ko.bindingHandlers.largeNumber = {
init: function(element, valueAccessor) {
var value = ko.unwrap(valueAccessor());
var interceptor = ko.computed({
read: function() {
return formatLargeNumber(value);
},
write: function(newValue) {
value(reverseFormat(newValue));
}
});
if(element.tagName == 'input' )
ko.applyBindingsToNode(element, {
value: interceptor
});
else
ko.applyBindingsToNode(element, {
text: interceptor
});
}
}
Any ideas?
You have multiple problems with your current approach:
element.tagName returns INPUT, etc so you need to take care of the casing when doing the comparing.
var value = ko.unwrap(valueAccessor()); you are unwrapping your observable so in your computed you are using its value and not the function itself. So you just need var value = valueAccessor(); and you need to call ko.unwrap in your computed read method.
You don't just need to format but you need to "unformat" in the write method, but your formatLargeNumber only do the format direction.
You have applied value and your largeNumber on the same input which make the two bindings interfering with each other
Don't write the formatting code yourself just use a library which already does this like: http://numeraljs.com/
So here is the corrected version of your binding using numeraljs:
ko.bindingHandlers.largeNumber = {
init: function(element, valueAccessor) {
var value = valueAccessor();
var interceptor = ko.computed({
read: function() {
return numeral(ko.unwrap(value)).format('0,0');
},
write: function(newValue) {
value(numeral().unformat(newValue));
value.valueHasMutated();
}
}).extend({notify: 'always'});
if(element.tagName.toLowerCase() == 'input' )
ko.applyBindingsToNode(element, {
value: interceptor
});
else
ko.applyBindingsToNode(element, {
text: interceptor
});
}
}
And use it like this:
<input data-bind="largeNumber: testVal">
Demo JSFiddle.
I have these two bindings that use the same init code, and are bound to the same value, but they don't init the same. The first behaves normally, the second one runs its update instead of init. Here are the bindings:
function initToggle(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
};
//Binding Handlers
ko.bindingHandlers.fadeVisible = {
init: initToggle,
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();
}
};
ko.bindingHandlers.slideVisibleVertical = {
init: initToggle,
update: function(element, valueAccessor, allBindingsAccessor) {
var value = valueAccessor(); //Lastest binding value
var allBindings = allBindingsAccessor(); //other bindings, allows options
var valueUnwrapped = ko.utils.unwrapObservable(value);
var duration = allBindings.slideDuration || 400; // 400ms is default duration unless otherwise specified
if (valueUnwrapped == true)
$(element).show('slide', {direction: 'up'}, duration);
else
$(element).hide('slide', {direction: 'up'}, duration);
}
};
Literally, same init function. Here is a fiddle showing two buttons, both bound to the same value. fadeVisible starts out as shown, but slidevisibleVertical slides in. Both animate when updating. Anyone know whats going on here?
The update function always runs the first time as well.
For your fadeVisible the element will already be visible, so fading it in will not do anything.
For the other binding calling slide on it will still animate it, even though it is shown.
Some different ways that you could handle it:
store something on the element ($data) to indicate that you are on the first pass and clear it as the end of the update (or vice-versa)
check the visibility before sliding (as you mentioned in the comments)
To clarify the accepted answer's comment about storing something on the element ($data), here is sample code:
ko.bindingHandlers.myHandler = {
init: function (element, valueAccessor) {
$(element).data("isFirstPass", true);
},
update: function (element, valueAccessor) {
// Always unwrap so that ko tracks the dependency
ko.unwrap(valueAccessor());
if ($(element).data("isFirstPass")) {
$(element).removeData("isFirstPass")
}
else {
// Do something cool
}
}
};
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