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>
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.
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.
I am trying to reference a backbone function from within a d3 function inside the backbone render function. I now must reference other Backbone functions, to do some backboney things, but can't access them by referencing it by using the this/that method (I use this/ƒthis):
define([
// this is for require.js file modularization
], function(){
return Backbone.View.extend({
initialize: function(options){
//CODE
this.render();
},
render: function(options){
// HOW I ACCESS THE BACKBONE VIEW IN NESTED SITUATIONS
var ƒthis = this;
//NORMAL RENDERING
if (!options) {
// Do some stuff, get some vars
// compile the template
// D3 stuff
var lineData = ({...});
var pathFunction = d3.svg.line()
var beatUnwindingPaths = [......];
var beatContainer = d3.select('#beatHolder'+this.parent.cid);
var beatPath = beatContainer //.append('g')
.insert('path', ':first-child')
.data([beatUnwindingPaths[0]])
.attr('d', pathFunction)
.attr('class', 'beat')
//THIS IS WHERE I WANT TO REFERENCE THE FUNCTION TO BE CALLED, AND HOW I THINK IT SHOULD BE CALLED
.on('click', ƒthis.toggle);
//BUT CURRENTLY I AM ONLY LIMITED TO CALLING A FUNCTION DECLARED WITHIN THE BACKBONE render func(), so it would look like this:
.on('click', toggle);
//CURRENTLY I AM HAVING TO DECLARE THE FUNCTIONS INSIDE RENDER
function unroll() {
//do some stuff
};
function reverse() {
};
$('#a').on('click', unroll);
$('#b').on('click', reverse);
}
},
// THIS IS THE FUNCTION I WANT TO CALL
toggle: function(){
//DO some stuff to the other BackBone models, collections and other cool stuff
}
});
});
How do I access the Backbone toggle function from inside the D3 code?
Error code is from within the toggle function itself (worked before, so I am trying to figure out why it isn't now), and the error is on 313, not 314, my browser console always is one line off. I put a console.log() to see that with the ƒthis.toggle I got in the function, but error-ed out on the switching of the bool value.
311 toggle: function(){
312 //switch the selected boolean value on the model
313 this.model.set('selected', !this.model.get('selected'));
314 //re-render it, passing the clicked beat to render()
315 this.render(this.model);
316 // log.sendLog([[1, "beat" + this.model.cid + " toggled: "+!bool]]);
317 dispatch.trigger('beatClicked.event');
318 }
I switched from the rendering the circle in the template, to having d3 create it (so we could animate it using the d3 functions), and I think somehow the object has lost its binding to the model. Working on this.....
This isn't a D3/Backbone issue, it's just Javascript. You can't pass an object method to be invoked later and expect this to work within that method unless you bind it in one way or another:
var myObject = {
method: function() {
this.doSomeStuff();
},
doSomeStuff: function() {
console.log("stuff!");
}
};
myObject.method(); // "stuff!"
// pass this method somewhere else - this is
// effectively what you do with an event handler
var myMethod = myObject.method;
myMethod(); // TypeError: Object [object global] has no method 'doSomeStuff'
The second part fails because the invocation myObject.myMethod() binds this to myObject, but assigning the method to a variable (or assigning it as an event handler) does not (in most cases, this is bound to window, but D3 will reassign this to the DOM element you set the handler on).
The standard fixes are 1) wrapping it in a function:
var myMethod = function() {
myObject.method();
};
myMethod(); // "stuff!"
or 2) binding it to the object somehow, e.g. in your Backbone initialize method (Underscore provides a useful _.bindAll utility for this purpose):
Backbone.View.extend({
initialize: function(options){
_.bindAll(this, 'toggle');
// now you can pass this.toggle around with impunity
},
// ...
});
I've got a few things interacting here, and they aren't interacting well.
I have a base class:
var ObjOne = (function() {
return function() {
var self = this;
self.propertyOne = ko.observable(1);
self.observable = ko.observable(1);
self.observable.subscribe(function(newValue) {
self.propertyOne(newValue);
});
};
} ());
It has two Knockout observables, and defines a subscribe on one of them that updates the other.
I have a "subclass", extended with jQuery.extend:
var ObjTwo = (function() {
return function() {
this.base = new ObjOne();
$.extend(this, this.base);
};
} ());
And I have a Jasmine test, which is attempting to ask the question "when I update observable, is propertyOne called?"
it('Test fails to call the correct propertyOne', function() {
var obj = new ObjTwo();
spyOn(obj, 'propertyOne').andCallThrough();
obj.observable(2);
expect(obj.propertyOne).toHaveBeenCalled();
expect(obj.propertyOne()).toBe(2);
});
This fails with "Expected spy propertyOne to have been called.". When I debug, the observable is updated properly. In the actual system, it works fine (as well, even the test "is propertyOne equal to 2?" passes. When I debug into the subscribe function, self.propertyOne is not a spy, but in the test, it is.
I have a solution, but it isn't great:
it('Test calls the base propertyOne', function() {
var obj = new ObjTwo();
spyOn(obj.base, 'propertyOne').andCallThrough();
obj.observable(2);
expect(obj.base.propertyOne).toHaveBeenCalled();
expect(obj.propertyOne()).toBe(2);
});
Note the .base added to the two lines. I don't like that I've had to expose the base class, or had to touch it's properties in order to make the test run.
Here's a jsfiddle: http://jsfiddle.net/4DrrW/23/. The question is - is there a better way of doing this?
After you call $.extend(this, this.base); your object basically looks like:
{
base: {
propertyOne: ko.observable(1),
observable: ko.observable(1)
},
propertyOne: base.propertyOne,
observable: base.observable
}
When you do a spyOn for propertyOne it replaces it with a wrapper. However, the subscription is set between the actual observables and would not have any way to call the wrapper.
If you do not want to access base, then I would just remove the test that the observable was called. Checking that the value is correct seems sufficient.
Otherwise, you would probably be better off mixing in ObjOne by calling its constructor with the new object's this like:
var ObjTwo = (function() {
return function() {
ObjOne.call(this);
};
} ());
Then, the test would be fine: http://jsfiddle.net/rniemeyer/z2GU3/
I've been implementing a form of a publisher/subscriber design pattern in jQuery. I'm basically building classes in Javascript utilizing CoffeeScript that serve as components on my page. i.e. Navigation, DataList, etc.
Instead of having DOM elements fire events, I have instances of these classes that use trigger on themselves to send custom events. These instances can then listen to each other and can update the DOM elements they own accordingly based on the changes in each others behavior!
I know this works as I have one of my components dispatching a custom event properly. However, I've ran into a snag. I've created another component and for the life of me I cannot figure out why it's event is not being fired.
This is the implementation of my class:
window.List = (function() {
List = function(element, settings) {
var _a, _b, _c;
this.list = $(element);
this.settings = jQuery.extend(List.DEFAULTS, settings);
this.links = this.list.find(this.settings.link_selector);
this.links.selectable();
_b = [SelectableEvent.COMPLETED, SelectableEvent.UNDONE, SelectableEvent.SELECTED, SelectableEvent.DESELECTED];
for (_a = 0, _c = _b.length; _a < _c; _a++) {
(function() {
var event_type = _b[_a];
return this.links.bind(event_type, __bind(function(event, selectable_event) {
return this.dispatch(selectable_event);
}, this));
}).call(this);
}
return this;
};
List.DEFAULTS = {
link_selector: "a",
completed_selector: ".completed"
};
List.prototype.change = function(mode, previous_mode) {
if (mode !== this.mode) {
this.mode = mode;
if (previous_mode) {
this.list.removeClass(previous_mode);
}
return this.list.addClass(this.mode);
}
};
List.prototype.length = function() {
return this.links.length;
};
List.prototype.remaining = function() {
return this.length() - this.list.find(this.settings.completed_selector).length;
};
List.prototype.dispatch = function(selectable_event) {
$(this).trigger(selectable_event.type, selectable_event);
return alert(selectable_event.type);
};
return List;
}).call(this);
Pay attention to:
List.prototype.dispatch = function(selectable_event) {
$(this).trigger(selectable_event.type, selectable_event);
return alert(selectable_event.type);
};
This code is triggered properly and returns the expected event type via an alert. But before the alert it is expected to trigger a custom event on itself. This is where I'm encountering my problem.
$(document).ready(function() {
var list_change_handler, todo_list;
todo_list = new List("ul.tasks");
list_change_handler = function(event, selectable_event) {
return alert("Hurray!");
};
$(todo_list).bind(SelectableEvent.COMPLETED, list_change_handler);
$(todo_list).bind(SelectableEvent.UNDONE, list_change_handler);
$(todo_list).bind(SelectableEvent.SELECTED, list_change_handler);
$(todo_list).bind(SelectableEvent.DESELECTED, list_change_handler);
}
You see here the alert "Hurray" is what I want to fire but unfortunately I am having no luck here. Ironically I've done the exact same thing with another class implemented the same way dispatching a custom event and the listener is receiving it just fine. Any ideas on why this wouldn't work?
Update:
Per discussing in the comments, it looks like Logging "this" in console returns the JS Object representing the class. But logging "$(this)" returns an empty jQuery object, thus trigger would never be fired. Any thoughts on why $(this) is coming up empty when "this" is accurately returning the instance of the class?
I found out that jQuery could not index my object because the class implemented it's own version of a jQuery method. In this case, length(). Renaming the length() method to total() resolved the problem completely and any instance of the class can successfully trigger events.