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.
Related
I have a custom binding handler that I am binding to a complex object in my view model.
The binding handler works correctly and the update function is called when any of the observable's properties update. However, the update function is called for every updated property, leading to odd behaviour since I am relying on the entire object to be available and up to date.
I understand why this is happening, as each property is causing an update to be called, and I think I know how to prevent this - by using the deferred updates functionality of Knockout.
However, I am unable to find how to enable deferred updates just for the observable in my custom binding. I do not want to enable it application wide as I am writing the binding as a library function.
I have tried many different methods including:
trying to extend the binding handler itself;
extending the init function;
extending the valueAccessor;
replacing the valueAccessor with a new observable with deferred applied;
creating a computed observable and rebinding the element;
All of which have not worked.
I have not found any other custom binding handler that comes remotely close to this sort of function and have been trying to piece it together from other functions.
My binding code itself is relatively simple, I am taking the bound object and simply splitting out the parameters and passing them to a Code Mirror instance.
ko.bindingHandlers.editor = {
init: function(element, valueAccessor, allBindingsAccessor) {
var observableValue = ko.utils.unwrap(valueAccessor());
initEditor(element, observableValue, allBindingsAccessor);
},
update: function(element, valueAccessor, allBindingsAccessor) {
var observableValue = ko.unwrap(valueAccessor());
createEditor(codeEditorDiv, observableValue);
resize();
updateEditor(element, observableValue, allBindingsAccessor);
}
};
And my HTML code is:
<div id="editor" data-bind="editor: EditorVM"></div>
I am using Dotnetify for the ViewModel so it is a reasonable complex C# class, but suffice it to say that the binding is working and updating, but I need it to only call 'update' once all properties have been updated.
It's unfortunate you haven't shown what initEditor, createEditor and updateEditor do with the observableValue, because that's probably where you should be extending your observables.
The init and update methods of a binding create computed dependencies, meaning that any observable that is unwrapped in the call stack starting from init will cause the update method to be called.
In an abstract example:
const someVM = ko.observable({
a: ko.observable(1),
b: ko.observable(2),
c: ko.observable(3)
});
// Some function that unwraps properties
const logABC = function(vm) {
console.log(
vm.a(),
vm.b(),
vm.c()
);
}
// Regular binding update:
ko.computed(function update() {
console.log(
"Regular binding update:",
)
logABC(someVM())
});
// Change VM
someVM(someVM());
// Change a, b, and c
someVM().a("A");
someVM().b("B");
someVM().c("C");
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
Note that update is called:
When initializing the computed
When changing the observable that contains the viewmodel
When changing any of the observable properties of the viewmodel
There are several ways of solving the issue, of which the simplest is to create your own computed inside the init method of your binding and extend it to be deferred.
const someVM = ko.observable({
a: ko.observable(1),
b: ko.observable(2),
c: ko.observable(3)
});
const getABC = function(vm) {
return [vm.a(), vm.b(), vm.c()].join(", ");
}
ko.bindingHandlers.renderABC = {
init: function(el, va) {
el.innerText += "Init.\n";
// This ensures any inner unwrapping gets deferred
var updateSub = ko.computed(function update() {
el.innerText += getABC(ko.unwrap(va())) + "\n";
}).extend({ deferred: true });
ko.utils.domNodeDisposal.addDisposeCallback(el, function() {
updateSub.dispose();
});
}
}
ko.applyBindings({ someVM: someVM });
// Change VM
someVM(someVM());
// Change a, b, and c
someVM().a("A");
someVM().b("B");
someVM().c("C");
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<pre data-bind="renderABC: someVM"></pre>
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 have a complex javascript variable that has numerous nested variables inside of it, which each can contain their own nested variables... When I do my knockout mapping sometimes the nested variables are set as objects and other times those exact same variables are functions. So when my data bindings to those nested variables is expecting an object the next time around when the variable is now a function the binding does not update.
The below fiddle gives an idea of what I am seeing:
http://jsfiddle.net/Eves/AUcGM/2/
html:
<p> <span>Name:</span>
<span data-bind="text: TempData().objectA.Name"></span>
<button id="update" data-bind="click: Update">Update!</button>
</p>
Javascript:
var ViewModel = function (data) {
var me = this;
me.TempData = ko.observable(data);
me.Update = function () {
ko.mapping.fromJS(temp2, {}, me);
};
return me;
};
var temp1 = {
objectA: {
Name: 'temp1.objectA.Name'
}
};
var temp2 = {
objectA: function () {
this.Name = 'temp2.objectA.Name';
return this;
}
};
window.viewModel = ko.mapping.fromJS(new ViewModel(temp1));
ko.applyBindings(window.viewModel);
Initially the span text shows "temp1.objectA.Name". However, if you click the update button and the binding switches from the temp1 object to the temp2 object because "objectA" is now a function the data binding never updates. If I were to change the span's data binding to be "TempData().objectA().Name" then temp2 will work fine...but then that breaks temp1.
So the question is:
Is there a way to always evaluate a variable as either an object or function?
I suspect I could use ko.computed to always get the appropriate value regardless of function or object. But that would get really messy considering the complexity of the object I am really dealing with.
I didn't read your entire question but there is a quick answer -
var returnedValue = ko.unwrap(possibleFunction);
(In older versions of ko this is ko.utils.unwrapObservable)
A good way that you could use this would be in a custom binding handler to unwrap a value, regardless of whether it is a function or not.
ko.bindingHandlers.returnAction = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
var value = ko.unwrap(valueAccessor());
// now value is a value no matter what the element's value was set as
$(element).keydown(function (e) {
if (e.which === 13) {
value(viewModel);
}
});
}
};
Edit
Here is a working fiddle example of passing in various objects - http://jsfiddle.net/5udhU/3/
That demonstrates passing a string, an observable, a computed, and a function that all evaluate properly.
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/
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