Why assigning the same value triggers subscribe handler? - javascript

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);
};

Related

ag-Grid onCellEditingStopped event does not provide previous value

I am using ag-Grid onCellEditingStopped event handler to get the changed value of a grid cell.
onCellEditingStopped: function(event) {
// event.value present the current cell value
console.log('cellEditingStopped');
}
But it does not provide the previous value (the value before the change happens). Is there anyway to get the previous value ?
My current solution:
I am using onCellEditingStarted event to store the current cell value in a separate variable and use that variable inside the onCellEditingStopped event handler function. But it is not a clear solution.
Thanks
you can use value Setter function for that column as below.
valueSetter: function (params) {
console.log(params.oldValue);
console.log(params.newValue);
if (params.oldValue !== params.newValue) {
//params.data["comments"] = params.newValue;
return true;
}
else {
return false;
}
}
I'm using onCellEditingStopped in my Angular application, with agGrid v24.1... and it does have an oldValue and a newValue value in there.
onCellEditingStopped = (_params) => {
if (_params.newValue != _params.oldValue) {
// Do something...
}
}

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 subscribe triggers on page load for selectbox

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" });
};

Knockout JS and iPad update issue on text area / wysiwyg

i have an application built with knockout that utilises a wysiwyg called redactor (not that I think that is relevant to this problem but just in case)
I have just run into an issue where a user has an iPad and when they save a 'note' it is missing the last letter each time - nobody else has ever had this issue.
It is evident that this is down to the underlying text area not updating on the last key press, but I am not sure how to fix this.
I have a custom binding as follows
ko.bindingHandlers.redactor = {
init: function(element, valueAccessor) {
var value = valueAccessor();
if (ko.isObservable(value)) {
$(element).redactor({
changeCallback: value,
fileUpload: 'url',
fileManagerJson: site_URL + 'files/files.json',
plugins: ['filemanager', 'clips', 'textexpander', 'bufferbuttons'],
textexpander: [
["##s", "<strong>(S)</strong> - "]
]
});
}
},
update: function(element, valueAccessor) {
var value = ko.utils.unwrapObservable(valueAccessor()) || '';
if (value !== $(element).redactor('core.getTextarea').val()) {
$(element).redactor('code.set', value );
}
}
}
How can i change this to make sure it behaves as expected on the iPad and updates on the final key press? Or is there a way of simply forcing the update on keyPress / keyDown within an update.
The code samples in the documentation retrieve the widget value like this:
$('#redactor').redactor({
callbacks: {
change: function()
{
console.log(this.code.get());
}
}
});
In other words, they call this.code.get() in the callback body.
Your setup on the other hand...
$(element).redactor({
changeCallback: value
});
it implicitly uses the first argument to the callback as the value.
Does it make a difference if you use the same approach the documentation suggests?
$(element).redactor({
changeCallback: function () {
value(this.code.get());
});
});

Problems unwatching an object using watch.js

I'm trying to use watch.js in the code below. The idea is too run watch.js in a one shot way, so it detects the boolean change (of vars.in_animation) and then unwatchs and exits. However, what happens is the code falls into inifinite loop and everytime vars.in_animation changes the watch callback is triggered and the unwatch code never runs.
I don't know whether the problem is the way I'm using watch.js, or if there is some other issue in the structure of the code. Any ideas?
base.goTo = function(targetSlide){
if (vars.in_animation || !api.options.slideshow)
{
//arrange for a callback once in_animation is false again
//only need to detect first change from true to false, and then unwatch
watch(vars, "in_animation", function()
{
console.log('called back, value of in_animation ' + vars.in_animation);
unwatch(vars,"in_animation",vars.alert); //never called
base.goTo2(vars.saved_slide);
});
vars.saved_slide = targetSlide;
return false;
}
else { base.goTo2(targetSlide); }
}
EDIT, just made a fiddle of the problem > http://jsfiddle.net/SZ2Ut/3/
var obj = {
name: "buddy",
alert: function(){
alert(obj.phrase + " " + obj.name);
}
}
watch(obj, "name", function(){
alert('name has changed to: ' + obj.name);
});
//should only fire once for this one
obj.name = "johnny";
unwatch(obj, "name", obj.alert);
obj.name = "phil";
unwatch seems to expect the watcher callback that should be removed as its third argument. Try
watch(vars, "in_animation", function callback() {
console.log('called back, value of in_animation ' + vars.in_animation);
unwatch(vars,"in_animation", callback);
base.goTo2(vars.saved_slide);
});

Categories

Resources