Custom Knockout binding not working correctly - javascript

I created a computed observable that works well formatting phone numbers. It will take a string of numbers and strip it down to this format xxx-xxx-xxxx when focus is off the form field. then I a have a controller action stripping it down to format xxxxxxxxxx before it persists to the database. Then my computed observable re-formats it to the xxx-xxx-xxxx format.
I now want the to create a reusable custom binding handler that can be implemented across our application. The problem is that I cannot get it to do the last part where it re-formats it back in the form field. So, the issue is when I click update, the form field displays the number as xxxxxxxxxx (the same way as it is in the DB) Does anyone know what I need to change to make by custom binding work like my current computed observable?
Observable:
self.Phone = ko.observable(model.MainPhone ? model.MainPhone : "").extend({ maxLength: 20, minLength: 10 });
Computed Observable working correctly:
self.PhoneFormat = ko.computed(function () {
var phoneFormatting = self.Phone()
.replace(/\D+/g, "")
.replace(/^[01]/, "")
.replace(/(\d{3})(\d{3})(\d{4})/, "$1-$2-$3")
.substring(0, 12);
return self.Phone() ? self.Phone(phoneFormatting) : "";
}, self);
Custom binding not working correctly:
ko.bindingHandlers.formatPhone = {
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);
},
update: 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.update(element, formatter, allBindings);
}
};

It feels like the read and write should be swapped around...
I'd say you want your "source" data to be just the numbers. The formatted data is mainly for display purposes.
I'd expect a computed layer added in your binding, that does two things:
When writing, remove the formating before putting it in the source
When reading, add in the correct format
I'm not sure if I broke stuff that you had working previously, but it could be something like:
ko.bindingHandlers.formatPhone = {
init: function (element, valueAccessor, allBindings) {
var source = valueAccessor();
var formatter = ko.computed({
write: function(newValue) {
source(newValue.split("-").join(""));
},
read: function() {
return source().replace(/\D+/g, "")
.replace(/^[01]/, "")
.replace(/(\d{3})(\d{3})(\d{4})/, "$1-$2-$3")
.substring(0, 12);
}
});
ko.bindingHandlers.value.init(element, function() { return formatter; }, allBindings);
}
};
var vm = { phoneNr: ko.observable("3456546512") };
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
Via custom binding:
<input data-bind="formatPhone: phoneNr">
<br/>
Mimic external update:
<input data-bind="textInput: phoneNr">

I am not sure why this works but the custom binding did not. :(
HTML:
<input data-bind="value: Phone" />
KO Observable:
self.Phone = ko.observable(model.Phone ? model.Phone : "").trimmed();
KO Subscribable:
ko.subscribable.fn.trimmed = function () {
return ko.computed({
read: function () {
return this().replace(/\D+/g, "")
.replace(/^[01]/, "")
.replace(/(\d{3})(\d{3})(\d{4})/, "$1-$2-$3")
.substring(0, 12);
},
write: function (value) {
this(value.replace(/\D+/g, "")
.replace(/^[01]/, "")
.replace(/(\d{3})(\d{3})(\d{4})/, "$1-$2-$3")
.substring(0, 12));
this.valueHasMutated();
},
owner: this
}).extend({ notify: 'always' });
};

Related

JavaScript event, not updating knockout observable/model value

I have text field which is wired up with Keyup & change event, to trim the field length.
#Html.TextBoxFor(m => m.zipCode, new {data_bind = "textInput: zipcode, event: { keyup: trimField, change: trimField }", maxlength = "5"})
Trim function,
function trimField(data, event) {
var obj = event.target;
var maxlength = parseInt(obj.getAttribute('maxlength'), 10);
obj.value = obj.value.substring(0, maxlength);
obj.focus();
return true;
}
If I type "123456", on the UI it shows "12345", but the model has "123456".
How to get model updated after the keyup event?
You are not updating the observable variable which is bound to your element. It is better to make it generic as an observable extend so it can be used everywhere based on your max-length and to make sure it follows your rule for an initial value.
Example : https://jsfiddle.net/kyr6w2x3/55/
HTML:
<input data-bind='textInput: zipCode' />
<div>
zip code in Model:<span data-bind="text:zipCode"></span>
</div>
JS:
function AppViewModel(input) {
this.zipCode = ko.observable(input).extend({ maxLength:5});
this.phone = ko.observable(input).extend({ maxLength:11});
}
ko.extenders.maxLenght = function(target, characters) {
//you can use this to show an error message on view
// target.validationMessage = ko.observable();
//define a function to do validation for maxLength
function validate(newValue) {
var maxlength = parseInt(characters, 10);
if(newValue){
target(newValue.substring(0, maxlength) );
}
}
//initial validation
validate(target());
//validate whenever the value changes
target.subscribe(validate);
//return the original observable
return target;
};
ko.applyBindings(new AppViewModel("12345678910"));
change maxlength from 5 to 6:
#Html.TextBoxFor(m => m.zipCode, new {data_bind = "textInput: zipcode, event: { keyup: trimField, change: trimField }", maxlength = "6"})
Stop modifying the DOM. That's Knockout's job. You just modify the data item and Knockout will ensure that the UI is right.
function trimField(data, event) {
var obj = event.target;
var maxlength = parseInt(obj.getAttribute('maxlength'), 10);
data.zipcode(data.zipcode().substr(0, maxlength));
return true;
}

Detect input value change with MutationObserver

I want to detect when text/value change in input field. Even if I change the value with js, I want to detect that changes.
Here's what I've tried so far in demo in fiddle.
HTML:
<input type="text" id="exNumber"/>
JavaScript:
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
// console.log('Mutation type: ' + mutation.type);
if ( mutation.type == 'childList' ) {
if (mutation.addedNodes.length >= 1) {
if (mutation.addedNodes[0].nodeName != '#text') {
// console.log('Added ' + mutation.addedNodes[0].tagName + ' tag.');
}
}
else if (mutation.removedNodes.length >= 1) {
// console.log('Removed ' + mutation.removedNodes[0].tagName + ' tag.')
}
}
if (mutation.type == 'attributes') {
console.log('Modified ' + mutation.attributeName + ' attribute.')
}
});
});
var observerConfig = {
attributes: true,
childList: false,
characterData: false
};
// Listen to all changes to body and child nodes
var targetNode = document.getElementById("exNumber");
observer.observe(targetNode, observerConfig);
To understand what is going on is necessary to clear up the difference between attribute (content attribute) and property (IDL attribute). I won't expand on this as in SO there are already excellent answers covering the topic:
Properties and Attributes in HTML
.prop() vs .attr()
What is happening behind .setAttribute vs .attribute=?
When you change the content of a input element, by typing in or by JS:
targetNode.value="foo";
the browser updates the value property but not the value attribute (which reflects the defaultValue property instead).
Then, if we look at the spec of MutationObserver, we will see that attributes is one of the object members that can be used. So if you explicitly set the value attribute:
targetNode.setAttribute("value", "foo");
MutationObserver will notify an attribute modification. But there is nothing like properties in the list of the spec: the value property can not be observed.
If you want to detect when an user alters the content of your input element, the input event is the most straightforward way. If you need to catch JS modifications, go for setInterval and compare the new value with the old one.
Check this SO question to know about different alternatives and its limitations.
I've modified Shawn's method a little and wanted to share it. Can't believe there's actually a solution to this.
Type into the input box to see the default behavior. Now, open the DevTools and select the input element, then change its value, e.g. $0.value = "hello". Examine the UI vs. API difference. It seems UI interactions do not modify value property directly. If it were, it would also log "...changed via API...".
let inputBox = document.querySelector("#inputBox");
inputBox.addEventListener("input", function () {
console.log("Input value changed via UI. New value: '%s'", this.value);
});
observeElement(inputBox, "value", function (oldValue, newValue) {
console.log("Input value changed via API. Value changed from '%s' to '%s'", oldValue, newValue);
});
function observeElement(element, property, callback, delay = 0) {
let elementPrototype = Object.getPrototypeOf(element);
if (elementPrototype.hasOwnProperty(property)) {
let descriptor = Object.getOwnPropertyDescriptor(elementPrototype, property);
Object.defineProperty(element, property, {
get: function() {
return descriptor.get.apply(this, arguments);
},
set: function () {
let oldValue = this[property];
descriptor.set.apply(this, arguments);
let newValue = this[property];
if (typeof callback == "function") {
setTimeout(callback.bind(this, oldValue, newValue), delay);
}
return newValue;
}
});
}
}
<input type="text" id="inputBox" placeholder="Enter something" />
the value property can be observed, Don't waste your time.
function changeValue (event, target) {
document.querySelector("#" + target).value = new Date().getTime();
}
function changeContentValue () {
document.querySelector("#content").value = new Date().getTime();
}
Object.defineProperty(document.querySelector("#content"), "value", {
set: function (t) {
alert('#changed content value');
var caller = arguments.callee
? (arguments.callee.caller ? arguments.callee.caller : arguments.callee)
: ''
console.log('this =>', this);
console.log('event => ', event || window.event);
console.log('caller => ', caller);
return this.textContent = t;
}
});
<form id="form" name="form" action="test.php" method="post">
<input id="writer" type="text" name="writer" value="" placeholder="writer" /> <br />
<textarea id="content" name="content" placeholder="content" ></textarea> <br />
<button type="button" >Submit (no action)</button>
</form>
<button type="button" onClick="changeValue(this, 'content')">Change Content</button>
This works and preserves and chains the original setter and getter so everything else about your field still works.
var registered = [];
var setDetectChangeHandler = function(field) {
if (!registered.includes(field)) {
var superProps = Object.getPrototypeOf(field);
var superSet = Object.getOwnPropertyDescriptor(superProps, "value").set;
var superGet = Object.getOwnPropertyDescriptor(superProps, "value").get;
var newProps = {
get: function() {
return superGet.apply(this, arguments);
},
set: function (t) {
var _this = this;
setTimeout( function() { _this.dispatchEvent(new Event("change")); }, 50);
return superSet.apply(this, arguments);
}
};
Object.defineProperty(field, "value", newProps);
registered.push(field);
}
}

Knockout check box binding

I am trying to have a checkbox update the value of an observable. This partially works, but the checkbox does not "check" itself after. To solve this I tried to add the checked: binding, looking for the value that I had just set in the click event, but this also does not work.
My Observable
appViewModel.test = ko.observable(1);
The checkbox
<input type="checkbox" data-bind="checked: test() == 4, click: test.bind($data, 4)"/>
You can write a click handler that checks to see if the value is 4 (or whatever arbitrary value you want) and then can act accordingly, like this:
HTML:
<input type="checkbox" data-bind="checked: checkBoxValue() === 4,
click: handleCheckBoxClick">
<br/>
<div>
<span>Debug:</span>
<pre data-bind="text: ko.toJSON($root, null, 2)"></pre>
</div>
JavaScript:
var ViewModel = function () {
var self = this;
self.checkBoxValue = ko.observable(0);
self.handleCheckBoxClick = function () {
if (self.checkBoxValue() !== 4) {
self.checkBoxValue(4);
} else {
self.checkBoxValue(0);
}
return true;
};
};
ko.applyBindings(new ViewModel());
Note: I added the debug output so you could see the underlying checkBoxValue value in the view model as you interact with the checkbox.
See jsFiddle here
You can use a computed to catch observable changes:
var ViewModel = function(first, last) {
var self = this;
this.checked = ko.observable(false);
this.test = ko.observable();
this.isChecked = ko.computed(function(){
var test = self.test();
if(test === '4')
{
self.checked(true);
return;
}
self.checked(false);
});
};
Here is a jsfiddle
The checked binding wants to be two-way, writable as well as readable. It can't write to a test, though, when you click it. Instead of having a click binding, you should have the test set to 4 by the write function of the computed you use as the checked binding.
var vm = {
test: ko.observable(1)
};
vm.checked = ko.computed({
read: function () { return vm.test() == 4; },
write: function (newValue) {
vm.test(newValue ? 4 : 1);
}
});
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<input type="checkbox" data-bind="checked: checked"/>
<div data-bind="text:test"></div>

knockout chaning binding events

Currently I am unable to fire off a binding event that is dependent on the results of another binding event in knockout.
In the example below, provide a value in the 'available' input, when the 'condition1' input is filled with a value such as 22 the 'available' input should be cleared and disabled, all of this is done in the skip logic binding. This is happening properly.
However, the problem lies with the execution of the skiplogic binding on the chain1 input element. This isn't even being fired after the 'available' input is cleared of its value. How can I get it so the results of one binding fires off another binding?
Here is the js fiddle version of the code below: http://jsfiddle.net/gYNb8/2/
Here is the form I am using to test the concept out on:
<script src="http://code.jquery.com/jquery-2.0.3.min.js"></script>
<span>Condition 1</span>
<input id="condition1" data-bind="value: condition1" />
<br/>
<span>Condition 2</span>
<input id="condition2" data-bind="value: condition2" />
<br/>
<span>Available?</span>
<input id="available" data-bind="value: available, skipLogic: condition1, skipLogic: condition2" />
<br/>
<span>Chain1</span>
<input id="chain1" data-bind="value: chain1, skiplogic: available" />
Here is the javascript:
// Overall viewmodel for this screen, along with initial state
function ReservationsViewModel() {
var self = this;
self.condition1 = ko.observable();
self.condition2 = ko.observable();
self.available = ko.observable();
self.chain1 = ko.observable();
}
//Here are the conditions which govern whether an element should be enabled or not
var elementConditions = {
'available': [{
'Condition': 'condition1() > 0',
'Type': 'evaluation'
}, {
'Condition': 'condition2() > 0',
'Type': 'evaluation'
}],
'chain1': [{
'Condition': 'available',
'Type': 'empty'
}]
};
ko.bindingHandlers.skipLogic = {
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
var id = $(element).attr("id");
var conditions = elementConditions[id];
var isSkipped = false;
var conditionMet = false;
for (var i = 0; i < conditions.length; i++) {
conditionMet = false;
if (conditions[i].Type == "evaluation") {
conditionMet = eval('viewModel.' + conditions[i].Condition);
} else if (conditions[i].Type == "empty") {
if ($('#' + conditions[i].Condition).val().length == 0) {
conditionMet = true;
}
} else if (conditions[i].Type == "notempty") {
if ($('#' + conditions[i].Condition).val().length > 0) {
conditionMet = true;
}
}
if (conditionMet == true) {
isSkipped = true;
}
}
if (isSkipped) {
eval("viewModel." + id + "('');");
$(element).attr("disabled", "disabled");
} else {
if (elementSkipped[id] > 0) {
$(element).attr("disabled", "disabled");
} else {
$(element).removeAttr("disabled");
}
}
}
};
ko.applyBindings(new ReservationsViewModel());
Instead of trying to keep the conditions separately, can you use Boolean logic to string them together in the binding? this way you don't need to keep track of each bindings state. I have built the following binding:
ko.bindingHandlers.skipLogic = {
init: function(element, valueAccessor) {
},
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
var valueOfCondition = ko.unwrap(valueAccessor());
var jqElement = $(element);
//update if the field is disabled if more than one condition is met
if(valueOfCondition){
jqElement.prop('disabled', true);
}
else{
jqElement.prop('disabled', false);
}
}
};
a working example is here: http://jsfiddle.net/M7vUV/3/
The update function for a binding will be executed when the element is first bound (after the init function) and then will run again whenever any of its dependencies change. You create dependencies by accessing observables within the function (like inside of a computed, because a computed is actually used to facilitate the binding updates).
So, you would want to ensure that you are accessing the retrieving whatever you passed to the binding by calling valueAccessor(), then if the value is observable you would want to call it as a function to retrieve the value. Otherwise, if you unsure whether you have been passed an observable you can call ko.unwrap (before 2.3 this was ko.utils.unwrapObservable - post 2.3 either can be used).
Additionally, you could access values passed to other bindings by using the allBindingsAccessor argument (3rd argument) or accessing values directly off of the data (4th arg) or context (5th arg).
It will not work to pass multiple bindings with the same name of the same element. You might want to consider structuring it differently like passing an array data-bind="skipLogic: [one, two]" and then accessing the values off of each.

Bind any key (by keycode) to an action using Knockout

I'm looking for a method to bind many different keys to different actions/functions in my viewmodel.
I've found this example where a binding handler is used to bind actions to the enter-key.
But how do I modify this handler to also support a supplied key-code? I'd like to be able to use the same handler for all kinds of keys and preferably also combined with modifier keys.
ko.bindingHandlers.executeOnEnter = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
var allBindings = allBindingsAccessor();
$(element).keypress(function (event) {
var keyCode = (event.which ? event.which : event.keyCode);
if (keyCode === 13) {
allBindings.executeOnEnter.call(viewModel);
return false;
}
return true;
});
}
};
You could do something like this:
ko.bindingHandlers.actionKey = {
init: function(element, valueAccessor, allBindings, data) {
var handler = function(data, event) {
var keys = ko.utils.unwrapObservable(allBindings().keys) || [13]; //default to Enter Key
if (!Array.isArray(keys))
keys = [keys];
if (keys.indexOf(event.keyCode) > -1) {
valueAccessor().call(data, data, event);
};
};
var newValueAccessor = function() {
return { keyup: handler };
};
ko.bindingHandlers.event.init(element, newValueAccessor, allBindings, data);
}
};
and you can use this binding in like this:
Observable Keys: <input data-bind="actionKey: action, keys: keyCodes" /><br/>
Inline Keys: <input data-bind="actionKey: action, keys: [33, 34]" /><br/>
Inline Key: <input data-bind="actionKey: action, keys: 33" /><br/>
Default Keys: <input data-bind="actionKey: action" /><br/>
Here is a fiddle demonstrating this binding.

Categories