Knockout subscribe triggers on page load for selectbox - javascript

I have created an extender using subscribe with the purpose of intercept a user change of an input field and forcing a value to be set.
For example, I have set a forced value of "Bar" on an observable. If the user changes it to "Foo" I will force it to "Bar" and notify the user.
(it's for a prototype/example solution).
It works for text boxes and radio buttons but not for selectboxes. It seems to be triggered on page load, not only on change.
Fiddle: https://jsfiddle.net/4gus57pd/7/
ko.extenders.forcedValue = function(target, newValue) {
target.subscribe(function(writtenValue) {
console.log(target()+" - "+newValue);
if(newValue != writtenValue){
alert("In this example we will use the value "+newValue+" instead.");
}
target(newValue);
});
return target;
};
this.textbox = ko.observable("Foo").extend({ forcedValue: "Bar" });
this.selectbox = ko.observable(1).extend({ forcedValue: 2 });
When you run it the select box extender is triggered despite the value hasn't been changed?

The value attribute is a string, so knockout will try to match the values ["1", "2", "3"] to your observable, which holds either the number 1 or 2. It doesn't see a selected value, so it updates your observable to the first <option>.
For a quick solution, change your selectbox to ko.observable("1") and the forced value to "2".
Additional advice, not related to the problem:
Your current extender will always trigger two subscription changes. One for the first change, and one for the actual reset. If you wrap the observable in a read/write computed, you counter this:
ko.extenders.forcedValue = function(target, forcedValue) {
return ko.computed({
read: target.extend({ notify: "always" }),
write: function(newValue) {
console.log("Attempt:", target(), "->", newValue);
if (newValue !== forcedValue) {
console.log("Using " + forcedValue + " instead.");
target(forcedValue);
} else {
target(newValue);
}
}
}).extend({ notify: "always" });
};

Related

Event when input value is changed by JavaScript?

What is the event when an <input> element's value is changed via JavaScript code? For example:
$input.value = 12;
The input event is not helping here because it's not the user who is changing the value.
When testing on Chrome, the change event isn't fired. Maybe because the element didn't lose focus (it didn't gain focus, so it can't lose it)?
There is no built-in event for that. You have at least four choices:
Any time you change $input.value in code, call the code you want triggered by the change
Poll for changes
Give yourself a method you use to change the value, which will also do notifications
(Variant of #3) Give yourself a property you use to change the value, which will also do notifications
Of those, you'll note that #1, #3, and #4 all require that you do something in your code differently from just $input.value = "new value"; Polling, option #2, is the only one that will work with code that just sets value directly.
Details:
The simplest solution: Any time you change $input.value in code, call the code you want triggered by the change:
$input.value = "new value";
handleValueChange();
Poll for changes:
var last$inputValue = $input.value;
setInterval(function() {
var newValue = $input.value;
if (last$inputValue != newValue) {
last$inputValue = newValue;
handleValueChange();
}
}, 50); // 20 times/second
Polling has a bad reputation (for good reasons), because it's a constant CPU consumer. Modern browsers dial down timer events (or even bring them to a stop) when the tab doesn't have focus, which mitigates that a bit. 20 times/second isn't an issue on modern systems, even mobiles.
But still, polling is an ugly last resort.
Example:
var $input = document.getElementById("$input");
var last$inputValue = $input.value;
setInterval(function() {
var newValue = $input.value;
if (last$inputValue != newValue) {
last$inputValue = newValue;
handleValueChange();
}
}, 50); // 20 times/second
function handleValueChange() {
console.log("$input's value changed: " + $input.value);
}
// Trigger a change
setTimeout(function() {
$input.value = "new value";
}, 800);
<input type="text" id="$input">
Give yourself a function to set the value and notify you, and use that function instead of value, combined with an input event handler to catch changes by users:
$input.setValue = function(newValue) {
this.value = newValue;
handleValueChange();
};
$input.addEventListener("input", handleValueChange, false);
Usage:
$input.setValue("new value");
Naturally, you have to remember to use setValue instead of assigning to value.
Example:
var $input = document.getElementById("$input");
$input.setValue = function(newValue) {
this.value = newValue;
handleValueChange();
};
$input.addEventListener("input", handleValueChange, false);
function handleValueChange() {
console.log("$input's value changed: " + $input.value);
}
// Trigger a change
setTimeout(function() {
$input.setValue("new value");
}, 800);
<input type="text" id="$input">
A variant on #3: Give yourself a different property you can set (again combined with an event handler for user changes):
Object.defineProperty($input, "val", {
get: function() {
return this.value;
},
set: function(newValue) {
this.value = newValue;
handleValueChange();
}
});
$input.addEventListener("input", handleValueChange, false);
Usage:
$input.val = "new value";
This works in all modern browsers, even old Android, and even IE8 (which supports defineProperty on DOM elements, but not JavaScript objects in general). Of course, you'd need to test it on your target browsers.
But $input.val = ... looks like an error to anyone used to reading normal DOM code (or jQuery code).
Before you ask: No, you can't use the above to replace the value property itself.
Example:
var $input = document.getElementById("$input");
Object.defineProperty($input, "val", {
get: function() {
return this.value;
},
set: function(newValue) {
this.value = newValue;
handleValueChange();
}
});
$input.addEventListener("input", handleValueChange, false);
function handleValueChange() {
console.log("$input's value changed: " + $input.value);
}
// Trigger a change
setTimeout(function() {
$input.val = "new value";
}, 800);
<input type="text" id="$input">
Based on #t-j-crowder and #maciej-swist answers, let's add this one, with ".apply" function that prevent infinite loop without redefining the object.
function customInputSetter(){
var descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value");
var originalSet = descriptor.set;
// define our own setter
descriptor.set = function(val) {
console.log("Value set", this, val);
originalSet.apply(this,arguments);
}
Object.defineProperty(HTMLInputElement.prototype, "value", descriptor);
}
I'd add a 5th option based on T.J. Crowder suggestions.
But instead of adding new property You could change the actual "value" property to trigger additional action when set - either for the specific input element, or for all input objects:
//First store the initial descriptor of the "value" property:
var descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value");
var inputSetter = descriptor.set;
//Then modify the "setter" of the value to notify when the value is changed:
descriptor.set = function(val) {
//changing to native setter to prevent the loop while setting the value
Object.defineProperty(this, "value", {set:inputSetter});
this.value = val;
//Custom code triggered when $input.value is set
console.log("Value set: "+val);
//changing back to custom setter
Object.defineProperty(this, "value", descriptor);
}
//Last add the new "value" descriptor to the $input element
Object.defineProperty($input, "value", descriptor);
Instead of changing the "value" property for specific input element, it can be changed generically for all input elements:
Object.defineProperty(HTMLInputElement.prototype, "value", descriptor);
This method works only for change of value with javascript e.g. input.value="new value". It doesn't work when keying in the new value in the input box.
Here is a solution to hook the value property changed for all inputs:
var valueDescriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value");
HTMLInputElement.prototype.addInputChangedByJsListener = function(cb) {
if(!this.hasOwnProperty("_inputChangedByJSListeners")) {
this._inputChangedByJSListeners = [];
}
this._inputChangedByJSListeners.push(cb);
}
Object.defineProperty(HTMLInputElement.prototype, "value", {
get: function() {
return valueDescriptor.get.apply(this, arguments);
},
set: function() {
var self = this;
valueDescriptor.set.apply(self, arguments);
if(this.hasOwnProperty("_inputChangedByJSListeners")){
this._inputChangedByJSListeners.forEach(function(cb) {
cb.apply(self);
})
}
}
});
Usage example:
document.getElementById("myInput").addInputChangedByJsListener(function() {
console.log("Input changed to \"" + this.value + "\"");
});
One possible strategy is to use a mutationObserver to detect changes in attributes as follows:
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(){
console.log('hello')});
});
observer.observe($input, {
attributes: true
});
Although this in itself will not detect a change like:
$input.value = 12
it WILL detect a change of the actual value attribute:
$input.setAttribute('value', 12)
So if it is you that is setting the value programatically, just be sure to alter the attribute alongside the value = 12 statement and you can have the desired result.
A simple way is just to trigger an input event when you change the value.
You can do this in plain javascript.
Early in your script, put something like this:
let inputEvent = new Event('input',{bubbles:true,cancelable: true});
You can change 'input' to the event you want, 'change', 'blur', etc
Then, any time you change a value, just call this event on the same element
input.value = 12;// <- your example
input.dispatchEvent(inputEvent);// <- calling an event
This is vanilla javascript
I don't think there is an event that will cover all the programatically assigned scenarios for an input. But, I can tell you that you can programatically "fire" an event (a custom event, a regular event or just trigger an event handler[s])
It appears to me that you're using jQuery, and so, you could use:
$('input#inputId').val('my new value...').triggerHandler('change');
In that example, you're assigning a value, and forcing a call to the handler (or handlers) binded with the "change" event.
Event when input value is changed by JavaScript?
We can use event triggering with target.dispashEvent, as explained in this question.
Example with a text input:
const input = document.querySelector('.myTextInput');
//Create the appropriate event according to needs
const change = new InputEvent('change');
//Change the input value
input.value = 'new value';
//Fire the event;
const isNotCancelled = input.dispatchEvent(change);
When any handler on that event type doesn't use event.preventDefault(), isNotCancelled will be true, otherwise, it will be false.

Knockout append observables w/ characters

I have observables in my KnockoutJS app and their values are being fetched from an array that's contained globally in the app, eg:
self.observable = ko.observable(App[0].Observable_Value);
For ease, let's say Observable_Value = 10.
This is working as you'd expect it to and the <input> that self.observable is binded to has the correct value in it by default; 10.
What I want to do now is to append the observable with a % in the <input> so the displayed output in the input field will be 10%.
I simply want to append the input value with a % because I need the observable to be readable as a number and not a string to prevent NaN errors later on in the app. The app relies heavily on numbers.
I've tried doing this as a computed function with read/write & parseFloat but to no success.
Any ideas?
I think that the best option in this case would be a custom binding handler.
I simply want to append the input value with a % because I need the observable to be readable as a number and not a string
That is precisely what a binding handler could do - preserve the original value of the observable, but change the way that it is displayed.
I've tried doing this as a computed function
Although a computed function could do the trick, it's usually unecessary unless you want to actually work with the return value from the computed. In this case, since you just want to change the visual display, you don't need a another variable representing the same value.
Here is a very basic one that just puts a % sign in front of the observable's value. See fiddle
ko.bindingHandlers.percent = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var value = ko.unwrap(valueAccessor());
$(element).text('%' + value);
}
};
You can add a read/write computable that will add the percent for display, then remove it when setting the underlying value (fiddle):
ko.observable.fn.formatAsPercent = function() {
var base = this;
return ko.computed({
read: function() {
return base() + "%";
},
write: function(newValue) {
base(parseInt(newValue.replace('%', '')));
}
});
};
function ViewModel() {
var self = this;
self.number = ko.observable(10); // actual number
self.percent= self.number.formatAsPercent(); // used for binding to show %
}
var my = { vm : new ViewModel() };
ko.applyBindings(my.vm);

Why assigning the same value triggers subscribe handler?

I have a simple viewmodel with an observalbe array of items, and an observable holding the selected item. I subscribe to the changes of selected item, and I can see in my tests that the handler is fired even when I assign the same value again and again, so there should not be any change. The following code shows 3 alerts with all the same "changed to ..." text.
view.SelectedItem(view.Items()[0]);
view.SelectedItem.subscribe(function(newValue) {
alert("changed to " + ko.toJSON(newValue));
});
view.SelectedItem(view.Items()[0]);
view.SelectedItem(view.Items()[0]);
view.SelectedItem(view.Items()[0]);
Here is a demo fiddle.
Apparently, selecting an item, even if it's the same one as what's already selected, triggers the change event, calling the function specified when subscribing.
If you want to be notified of the value of an observable before it is about to be changed, you can subscribe to the beforeChange event. For example:
view.SelectedItem.subscribe(function(oldValue) {
alert("The previous value is " + oldValue);
}, null, "beforeChange");
Source
This could help you determine whether or not the value has changed.
You can create function to have access to old and new values for compare it:
ko.subscribable.fn.subscribeChanged = function(callback) {
var previousValue;
this.subscribe(function(oldValue) {
previousValue = oldValue;
}, undefined, 'beforeChange');
this.subscribe(function(latestValue) {
callback(latestValue, previousValue);
});
};
You could add this function to some file with you ko extensions. I once found it on stackoverflow but can't remeber link now. And then you could use it like this:
view.SelectedItem.subscribeChanged(function(newValue, oldValue) {
if (newValue.Name != oldValue.Name || newValue.Quantity != oldValue.Quantity) {
alert("changed to " + ko.toJSON(newValue));
}
});
Fiddle
I ended up creating my own method based on a thread on a forum:
// Accepts a function(oldValue, newValue) callback, and triggers it only when a real change happend
ko.subscribable.fn.onChanged = function (callback) {
if (!this.previousValueSubscription) {
this.previousValueSubscription = this.subscribe(function (_previousValue) {
this.previousValue = _previousValue;
}, this, 'beforeChange');
}
return this.subscribe(function (latestValue) {
if (this.previousValue === latestValue) return;
callback(this.previousValue, latestValue);
}, this);
};

Knockout.js modify value before ko.observable() write

I've got a working viewmodel with numerous variables.
I use autoNumeric (http://www.decorplanit.com/plugin/) for text formatting in textbox. I'd like to use the input field's observed data in a computed observable, but if the observable textfield with the formatting gets modified, the formatting also gets saved in the variable.
How can I only observe the value of the input field without the formatting?
I think the best way to this could be a getter/setter to the observable, and remove the formatting when the value is set. I couldn't find a solution in knockout's documentation to write get/set methods for ko.observable(), and ko.computed() can not store a value.
I don't want to use hidden fields, or extra variables.
Is this possible without it?
Solution, as seen on http://knockoutjs.com/documentation/extenders.html
ko.extenders.numeric = function(target, precision) {
//create a writeable computed observable to intercept writes to our observable
var result = ko.computed({
read: target, //always return the original observables value
write: function(newValue) {
var current = target(),
roundingMultiplier = Math.pow(10, precision),
newValueAsNum = isNaN(newValue) ? 0 : parseFloat(+newValue),
valueToWrite = Math.round(newValueAsNum * roundingMultiplier) / roundingMultiplier;
//only write if it changed
if (valueToWrite !== current) {
target(valueToWrite);
} else {
//if the rounded value is the same, but a different value was written, force a notification for the current field
if (newValue !== current) {
target.notifySubscribers(valueToWrite);
}
}
}
});
//initialize with current value to make sure it is rounded appropriately
result(target());
//return the new computed observable
return result;
};
And later on
function AppViewModel(one, two) {
this.myNumberOne = ko.observable(one).extend({ numeric: 0 });
this.myNumberTwo = ko.observable(two).extend({ numeric: 2 });
}
You can use ko.computed() for this. You can specify a write option, see Writeable computed observables
Example (taken from knockout documentation):
function MyViewModel() {
this.price = ko.observable(25.99);
this.formattedPrice = ko.computed({
read: function () {
return '$' + this.price().toFixed(2);
},
write: function (value) {
// Strip out unwanted characters, parse as float, then write the raw data back to the underlying "price" observable
value = parseFloat(value.replace(/[^\.\d]/g, ""));
this.price(isNaN(value) ? 0 : value); // Write to underlying storage
},
owner: this
});
}
ko.applyBindings(new MyViewModel());

Backbone.js - custom setters

Imagine a simple backbone model like
window.model= Backbone.Model.extend({
defaults:{
name: "",
date: new Date().valueOf()
}
})
I'm trying to find a way to always make the model store the name in lower-case irrespective of input provided. i.e.,
model.set({name: "AbCd"})
model.get("name") // prints "AbCd" = current behavior
model.get("name") // print "abcd" = required behavior
What's the best way of doing this? Here's all I could think of:
Override the "set" method
Use a "SantizedModel" which listens for changes on this base model and stores the sanitized inputs. All view code would then be passed this sanitized model instead.
The specific "to lower case" example I mentioned may technically be better handled by the view while retrieving it, but imagine a different case where, say, user enters values in Pounds and I only want to store values in $s in my database. There may also be different views for the same model and I don't want to have to do a "toLowerCase" everywhere its being used.
Thoughts?
UPDATE: you can use the plug-in: https://github.com/berzniz/backbone.getters.setters
You can override the set method like this (add it to your models):
set: function(key, value, options) {
// Normalize the key-value into an object
if (_.isObject(key) || key == null) {
attrs = key;
options = value;
} else {
attrs = {};
attrs[key] = value;
}
// Go over all the set attributes and make your changes
for (attr in attrs) {
if (attr == 'name') {
attrs['name'] = attrs['name'].toLowerCase();
}
}
return Backbone.Model.prototype.set.call(this, attrs, options);
}
It would be a hack, because this isn't what it was made for, but you could always use a validator for this:
window.model= Backbone.Model.extend({
validate: function(attrs) {
if(attrs.name) {
attrs.name = attrs.name.toLowerCase()
}
return true;
}
})
The validate function will get called (as long as the silent option isn't set) before the value is set in the model, so it gives you a chance to mutate the data before it gets really set.
Not to toot my own horn, but I created a Backbone model with "Computed" properties to get around this. In other words
var bm = Backbone.Model.extend({
defaults: {
fullName: function(){return this.firstName + " " + this.lastName},
lowerCaseName: function(){
//Should probably belong in the view
return this.firstName.toLowerCase();
}
}
})
You also listen for changes on computed properties and pretty much just treat this as a regular one.
The plugin Bereznitskey mentioned is also a valid approach.

Categories

Resources