Revert change with Knockoutjs and bindingHandlers using computed - javascript

Let's say I have something like <input type="text" data-bind="format: quantity">, that uses an custom bindingHandlers called format defined like this:
var getElementValue = function($element) {
return $element.val().replace(/\$/g, '');
};
ko.bindingHandlers.format = {
init: function(element, bindingAccessor) {
var $element = $(element),
bindings = bindingAccessor();
$element.val('$' + ko.unwrap(bindings));
$element.change(function () {
bindings(getElementValue($element));
});
},
update: function(element, bindingAccessor) {
var $element = $(element),
bindings = bindingAccessor();
$element.val('$' + ko.unwrap(bindings));
}
};
And a view model like:
var ViewModel = function() {
var self = this;
self._quantity = ko.observable(0);
self.quantity = ko.computed({
read: self._quantity,
write: function(newValue) {
if (newValue < 0) {
self._quantity.valueHasMutated();
console.log('quantity is invalid');
return;
}
self._quantity(newValue);
}
});
}
Since negative quantity is not allowed, the idea would be to revert the input to it previous value if that is provided.
However, the self._quantity.valueHasMutated(); in the write function is not notifying the bindingHandlers update of a mutation.
Any ideas? I have a JSFiddle setup for more details.

The simplest solution is to tell knockout to react to changes also when the old value and the new value are the same, by using .extend({ notify: 'always' }) for both, the "backup" observable, and the computed to reflect the changes in the text input:
self._quantity = ko.observable(0).extend({ notify: 'always' });
self.quantity = ko.computed({
read: self._quantity,
write: function(newValue) {
if (newValue < 0) {
self._quantity(self._quantity.peek());
//self._quantity.valueHasMutated();
console.log('quantity is invalid');
return;
}
self._quantity(newValue);
}
}).extend({ notify: 'always' });
Fiddle: http://jsfiddle.net/k0deqt8x/
EDIT:
BTW, this solution also works when using ko.options.deferUpdates = true;
Alternatively, you cold use a more standard solution like the one created by rniemeyer here: http://www.knockmeout.net/2011/03/guard-your-model-accept-or-cancel-edits.html

I'm not exactly sure why exactly your code doesn't work, but I did try some stuff and came up with something that does...
Since your input is bound to quantity, it'd make sense to send out a message that this value changed. You can do so by calling notifySubscribers on the computed:
self.quantity.notifySubscribers(self.quantity.peek());
This still does feel a bit strange, and I'd suggest looking into an extender like the ones in these examples.
Here's an updated fiddle: http://jsfiddle.net/pxxnuu2z/

Related

Detect setting value of a field with getElementById(field_name).value and intercept/override it

I don't have a provision to not invoke the JS that tries to set a value of a field using:
getElementById(field_name).value = 'someValue'
So, as an alternative, I'm trying to see if there's a way to intercept/override the JS that sets the value of the field in the html and try to restrict it if certain conditions are met.
Please let me know if it's possible?
It may seem that this question is a duplicate of this and this but it's not. Those question's solution wants a change in the code, which is not an option for me.
I'm afraid you can't, so if you really cannot change the code setting it, you're stuck with the really ugly option of polling it and watching for changes. E.g., something vaguely like this:
function polledInterceptor(id, callback) {
var element = document.getElementById(id);
var value = element.value;
var timer = setInterval(function() {
if (value !== element.value) {
value = callback(element, value);
if (value !== null) {
element.value = value;
}
}
}, 30); // 30 = 30ms
return {
stop: function() {
clearTimeout(timer);
}
};
}
...and then use it like this:
var poller = polledInterceptor("field_name", function(element, oldValue) {
var newValue = element.value;
// ...decide what to do...
return "someValue"; // or return null to leave the value unchanged
});
when ready to stop:
poller.stop();

Adding Backbone events to an object, not registering "on" event when modified

I may possibly be misunderstanding the eventing system in Backbone, but when I try the following code nothing happens. Should it not fire some sort of change, update or reset event when I add a new property to my Backbone.Events extended object? Like it does with a model?
Coffeescript:
data = items: {}
eventedData = _.extend data, Backbone.Events
# It has an on method!
console.log eventedData.on.length > 0
# Running on with everything I could imagine happening
eventedData.on 'update change reset add', () ->
alert 'Yay!'
# Triggers nothing
eventedData.items.newthing = 'Testing'
The above code in a JSBin http://jsbin.com/qofad/1/edit
I tried using object.set, or applying it directly rather than in a subobject, but nowt. Any ideas?
Many thanks in advance :)
You are confusing the functionality of .get and .set and others in Views/Models/Collections with events.
This for example is how backbone would trigger a change event on a .set:
set: function(attr, value) {
this.attributes[attr] = value;
this.trigger('change:' attr, ...);
}
The code is a little more involved than that but that is what it is essentially doing. No magic there. Backbone doesn't have "magic" object observation stuff as you are expecting but that makes a lot of sense but goes out of the scope of answering the question.
Sorry I don't know how to write it in CoffeeScript and you have 2 errors in your jsbin, but here is an example in normal JavaScript:
var ItemModel = function() {
this.items = {}; // It is important this is created in instance-scope.
this.initialize();
};
_.extend(ItemModel.prototype, Backbone.Events, {
initialize: function() {},
set: function(attr, value, options) {
options = options || {};
var oldValue = this.items[attr];
this.items[attr] = value;
if (!options.silent) {
this.trigger('change', attr, value, {
oldValue: oldValue
});
this.trigger('change:' + attr, value, {
oldValue: oldValue
});
}
},
get: function(attr) {
return this.items[attr];
}
});
ItemModel.extend = Backbone.Model.extend;
var MyImplementationOfItem = ItemModel.extend({
initialize: function() {
this.on('change', this.onItemChange);
},
onItemChange: function(attr, value, options) {
alert(attr + ' changed to ' + value + ' from ' + options.oldValue);
}
});
var myImplementationOfItem = new MyImplementationOfItem();
// Wait 4s then trigger it.
setTimeout(function() {
myImplementationOfItem.set('hello', 'world');
}, 2000);
setTimeout(function() {
myImplementationOfItem.set('hello', 'universe');
}, 4000);
Fiddle: http://jsfiddle.net/5jaeu/1/
Note that this is a simplified re-implementation of get and set already in Views/Models/Collections. You are encouraged to use those instead!

Access local function inside ko.computed

How come this line of code doesnt work.
Im using durandal/knockout and i have a structure like this
define(function () {
var vm = function() {
compute: ko.computed(function() {
return _compute(1); // fail
});
var _compute= function(e) {
return e;
}
}
return vm;
});
Basically I am just trying to access the private method _compute - but KO.compute doesnt allow that?
Even if i make it public, I still cant access it.
I trying to implement revealing pattern in this, but still no luck!
var vm = function() {
compute: ko.computed(function() {
return this._compute(1); // still failing
});
this._compute= function(e) {
return e;
}
}
update: so far, only this one works
define(function () {
var vm = function() {
var self = this;
var self._compute= function(e) {
return e;
}
compute: ko.computed(function() {
return this._compute(1); // works
}, self);
}
but like I said, _compute is not meant to be exposed.
Update: actually its another error.
this one now works
define(function () {
var vm = function() {
var self = this;
var _compute= function(e) {
return e;
}
compute: ko.computed(function() {
return _compute(1); // works
});
}
Basically, just need to declare the private function before the ko.computed prop!
Thanks!
Additional Note:
Why does it need to be declared before the computed function? I prefer all my "properties" in the first lines while the functions in the bottom. It is neater i Think.
This syntax does not create a property when in a function:
compute: ko.computed(function() {
return _compute(1); // fail
});
You have to use = instead of :.
Try this
var vm = function() {
var self = this;
var _compute = function(e) {
return e;
}
this.compute = ko.computed(function() {
return _compute(1);
});
}
Also note that this is not how you should use a computed observable. It should contain calls to other observables!
From doc:
What if you’ve got an observable for firstName, and another for
lastName, and you want to display the full name? That’s where computed
observables come in - these are functions that are dependent on one or
more other observables, and will automatically update whenever any of
these dependencies change.

Updating dependencies of writable computed observable

I'm trying use knockout.js' computed observables to setup C#-like properties on a model. I want observable to only accept valid values. Simplified example:
function Model()
{
var a = 100;
var b = 200;
var $ = this;
this.a = ko.computed({
read: function(){return a},
write: function(value){
var newval = parseFloat(value, 10);
if(newval < 1000)
a = newval;
}});
this.b = ko.observable(b);
}
Writing to a hover does not update the bindings. Is it possible to enable changing a as if it were a regular member of the Model but with extra functionality attached?
I know I can use second observable to contain the actual value and rely on it's update mechanism, but this approach becomes cumbersome quickly with increasing number of computed properties of this kind.
Computed observable is not suitable for your example, since a computed observable is a function that should depend on one or more other observables.
You can instead use extenders to achieve this. Here's a fiddle with a demo.
ko.extenders.maxNumber = function(target, option) {
var result = ko.computed({
read: target,
write: function(value){
var newval = parseFloat(value, 10);
if (newval < option) {
target(newval);
} else {
alert('Number is to big');
}
}
});
result(target());
return result;
};
function Model() {
var a = 100;
this.a = ko.observable(a).extend({ maxNumber: 1000 });
}

Is there a Javascript equivalent of Ruby's andand?

In trying to make my Javascript unobtrusive, I'm using onLoads to add functionality to <input>s and such. With Dojo, this looks something like:
var coolInput = dojo.byId('cool_input');
if(coolInput) {
dojo.addOnLoad(function() {
coolInput.onkeyup = function() { ... };
});
}
Or, approximately equivalently:
dojo.addOnLoad(function() {
dojo.forEach(dojo.query('#cool_input'), function(elt) {
elt.onkeyup = function() { ... };
});
});
Has anyone written an implementation of Ruby's andand so that I could do the following?
dojo.addOnLoad(function() {
// the input's onkeyup is set iff the input exists
dojo.byId('cool_input').andand().onkeyup = function() { ... };
});
or
dojo.byId('cool_input').andand(function(elt) {
// this function gets called with elt = the input iff it exists
dojo.addOnLoad(function() {
elt.onkeyup = function() { ... };
});
});
I don't know Dojo, but shouldn't your first example read
dojo.addOnLoad(function() {
var coolInput = dojo.byId('cool_input');
if(coolInput)
coolInput.onkeyup = function() { ... };
});
Otherwise, you might end up trying to access the element before the DOM has been built.
Back to your question: In JavaScript, I'd implement andand() as
function andand(obj, func, args) {
return obj && func.apply(obj, args || []);
}
Your example could then be written as
dojo.addOnLoad(function() {
andand(dojo.byId('cool_input'), function() {
this.onkeyup = function() { ... };
});
});
which isn't really that much shorter than using the explicit if statement - so why bother?
The exact syntax you want is not possible in JavaScript. The way JavaScript executes would need to change in a pretty fundamental fashion. For example:
var name = getUserById(id).andand().name;
// ^
// |-------------------------------
// if getUserById returns null, execution MUST stop here |
// otherwise, you'll get a "null is not an object" exception
However, JavaScript doesn't work that way. It simply doesn't.
The following line performs almost exactly what you want.
var name = (var user = getUserById(id)) ? user.name : null;
But readability won't scale to larger examples. For example:
// this is what you want to see
var initial = getUserById(id).andand().name.andand()[0];
// this is the best that JavaScript can do
var initial = (var name = (var user = getUserById(id)) ? user.name : null) ? name[0] : null;
And there is the side-effect of those unnecessary variables. I use those variables to avoid the double lookup. The variables are mucking up the context, and if that's a huge deal, you can use anonymous functions:
var name = (function() {return (var user = getUserById(id)) ? user.name : null;})();
Now, the user variable is cleaned-up properly, and everybody's happy. But wow! what a lot of typing! :)
You want dojo.behavior.
dojo.behavior.add({
'#cool_input': {
onKeyUp: function(evt) { ... }
}
});
How about something like this:
function andand(elt, f) {
if (elt)
return f(elt);
return null;
}
Call like this:
andand(dojo.byId('cool_input'), function(elt) {
// this function gets called with elt = the input iff it exists
dojo.addOnLoad(function() {
elt.onkeyup = function() { ... };
});
});
As far as I know there isn't a built-in JavaScript function that has that same functionality. I think the best solution though is to query by class instead of id and use dojo.forEach(...) as you will be guaranteed a non-null element in the forEach closure.
You could always use the JavaScript equivalent:
dojo.byId('cool_input') && dojo.byId('cool_input').whateverYouWantToDo(...);
I've never used dojo, but most javascript frameworks (when dealing with the DOM) return the calling element when a method is called from the element object (poor wording, sorry). So andand() would be implicit.
dojo.addOnLoad(function() {
dojo.byId('cool_input').onkeyup(function(evt) { /*event handler code*/
});
});
For a list:
Array.prototype.andand = function(property, fn) {
if (this.filter(property).length > 0) this.map(fn);
}

Categories