2 states roundable numeric text box with knockoutjs - javascript

I want to have an html numeric text box with 2 states, when focused, it has to show all decimal places, and when focus is lost, only show 2 decimals. I've almost achieved it.
HTML:
<input data-bind="attr: { 'data-numericvalue': valueToRound}" class="numerictextbox"
type="number"/>
Javascript:
var viewModel = {
valueToRound: ko.observable(7.4267),
};
//NUMERIC TEXTBOX BEHAVIOUR
$('.numerictextbox').focusout(function () {
$(this).attr("data-numericvalue", this.value); //this line does not update the viewModel
this.value = parseFloat($(this).attr("data-numericvalue")).toFixed(2);
});
$('.numerictextbox').focusin(function () {
if ($(this).attr("data-numericvalue") !== undefined) this.value = $(this).attr("data-numericvalue");
});
ko.applyBindings(viewModel);
Jsfiddle: https://jsfiddle.net/7zzt3Lbf/64/
But my problem is that when focusout occurs, it doesn't update bound property, viewModel in this case. This is a simplified version of my code, so I want it to be generic for a lot of properties in my real scenario.

You're mixing in too much jQuery :)
Knockout has event bindings and a hasFocus binding to deal with UI input.
In the example below I've made a viewmodel that has a hidden realValue observable which stores the unmodified input. The displayValue limits this number to a 2 digit number when showDigits is false.
I've used hasFocus to track whether we want to show the whole number: it's linked to showDigits.
var ViewModel = function() {
this.showDigits = ko.observable(true);
var realValue = ko.observable(6.32324261);
this.displayValue = ko.computed({
read: function() {
return this.showDigits()
? realValue()
: parseFloat(realValue()).toFixed(2);
},
write: realValue
}, this);
};
ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<input data-bind="value: displayValue, hasFocus: showDigits" type="number"/>
Edit: After comment that a computed is too much extra code: here's how to wrap the computed logic in a reusable extender:
ko.extenders.digitInput = function(target, option) {
var realValue = target,
showRealValue = ko.observable(false),
displayValue = ko.computed({
read: function() {
return showRealValue()
? realValue()
: parseFloat(realValue()).toFixed(2);
},
write: realValue
}, this);
displayValue.showRealValue = showRealValue;
return displayValue;
};
var ViewModel = function() {
this.value1 = ko.observable(6.452345).extend({ digitInput: true });
this.value2 = ko.observable(4.145).extend({ digitInput: true });
};
ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<input data-bind="value: value1, hasFocus: value1.showRealValue" type="number"/>
<input data-bind="value: value2, hasFocus: value2.showRealValue" type="number"/>

Related

knockout.js how to set selected option after confirm() cancelled within a ko.computed write

i have a selector element with options and default text:
self._selected = ko.observable();
self.option = ko.computed({
read:function(){
return self._selected;
},
write: function(data){
if(data){
if(confirm('are you sure?')){
self._selected(data);
}else{
//reset
}
}
}
});
<select data-bind="options: options, value:option, optionsCaption: 'choose ...'"></select>
the problem this:
select "one"
on the confirm click cancel
the selected option is "one" still under focus
it should be "choose ..."
jsbin here, it was tested on chrome only
The problem is that the value of the underlying variable is not changing, so there's no event to tell Knockout that its value is out of sync with the viewmodel.
With a normal observable, you can call valueHasMutated to indicate that some occult change has happened, but computeds don't seem to have that. But they do have notifySubscribers. In fact, your example is very much like this example in the docs.
Here's a working example:
function vm() {
const self = {};
self.options = ko.observableArray(['one', 'two', 'three']);
self._selected = ko.observable();
self.option = ko.pureComputed({
read: self._selected,
write: function(data) {
if (data) {
if (confirm('are you sure?')) {
self._selected(data);
} else {
self.option.notifySubscribers(self._selected());
}
}
}
});
return self;
}
ko.applyBindings(vm());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<select data-bind="options: options, value:option, optionsCaption: 'choose ...'"></select>
<div data-bind="text:_selected"></div>
<div data-bind="text:option"></div>
There is an asymmetry here:
When you change the value of a select box, the DOM gets updated immediately and knockout afterwards (of course, knockout depends on the DOM change event). So when your code asks "Are you sure?", the DOM already has the new value.
Now, when you do not write that value to the observable bound to value:, the viewmodel's state does not change. And knockout only updates the DOM when an observable changes. So the DOM stays at the selected value, and the bound value in your viewmodel is different.
The easiest way out of this is to save the old value in a variable, always write the new value to the observable, and simply restore the old value if the user clicks "no". This way the asymmetry is broken and the DOM and the viewmodel stay in sync.
var AppData = function(params) {
var self = {};
var selected = ko.observable();
self.options = ko.observableArray(params.options);
self.option = ko.computed({
read: selected,
write: function(value) {
var oldValue = selected();
selected(value);
if (value !== oldValue && !confirm('are you sure?')) {
selected(oldValue);
}
}
});
return self;
};
// ----------------------------------------------------------------------
ko.applyBindings(new AppData({
options: ['one','two','three']
}));
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<select data-bind="options: options, value: option, optionsCaption: 'Select...'"></select>
<hr>
<pre data-bind="text: ko.toJSON($root, null, 2)"></pre>
This is a perfect candidate for a knockout extender that asks for value change confirmation. This way we can re-use it for different observables and keep the viewmodel clean.
ko.extenders.confirmChange = function (target, message) {
return ko.pureComputed({
read: target,
write: function(newValue) {
var oldValue = target();
target(newValue);
if (newValue !== oldValue && !confirm(message)){
target(oldValue);
}
}
});
};
// ----------------------------------------------------------------------
var AppData = function(params) {
var self = this;
self.options = ko.observableArray(params.options);
self.option = ko.observable().extend({confirmChange: 'are you sure?'});
};
// ----------------------------------------------------------------------
ko.applyBindings(new AppData({
options: ['one','two','three']
}));
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<select data-bind="options: options, value: option, optionsCaption: 'Select...'"></select>
<hr>
<pre data-bind="text: ko.toJSON($root, null, 2)"></pre>

Create observable returning other observables as a single map

I have an own binding for numeric inputs made in knockoutJS which accepts only numbers.
To make big numbers I declare various instances of number in a NumberField like:
var NumberField = function () {
var self = this;
self.maskFormat = "0";
self.firstNumber = ko.observable("");
self.secondNumber = ko.observable("");
self.thirdNumber = ko.observable("");
};
And
<input id="0" maxlength="1" type="tel" data-bind="numeric: firstNumber">
<input id="1" maxlength="1" type="tel" data-bind="numeric: secondNumber">
<input id="2" maxlength="1" type="tel" data-bind="numeric: thirdNumber">
This is working like a charm, but when I made submission, system is expecting a map with numbers. I achieved it IMHO in an ugly way:
Added to NumberField this attribute:
this.cleanNumber = ko.pureComputed(function () {
return this.firstNumber().toString() + this.secondNumber().toString() + this.thirdNumber().toString();
}, this);
And in the code, when I need to use it I must do this:
let unwrapNumbers = this.numbers().cleanNumber().split("").map(function (item){
return Number(item);
});
This is working, but... I'm pretty sure there is an easier and more straight way.... Any suggestions?
I think it could help to split the computed in to two parts:
Getting the numbers you want to include in order
Creating a string based on the ordered values
Often it makes sense to split a computed in to several pure computeds that have a single clear data processing responsibility.
var NumberField = function () {
var self = this;
self.firstNumber = ko.observable(1);
self.secondNumber = ko.observable(2);
self.thirdNumber = ko.observable(3);
self.orderedNumbers = ko.pureComputed(function() {
return [self.firstNumber,
self.secondNumber,
self.thirdNumber].map(ko.unwrap);
});
self.cleanedNumber = ko.pureComputed(function() {
return self.orderedNumbers().join("");
});
};
var nf = new NumberField();
// If you want the numbers:
console.log(nf.orderedNumbers());
// If you want the string
console.log(nf.cleanedNumber());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
Now, I'm not sure what your requirements are, but you can take it one step further and use an observableArray as the base data format:
var NumberField = function () {
var self = this;
self.numbers = ko.observableArray(
[ko.observable(0), ko.observable(1), ko.observable(2)]);
self.add = function() {
self.numbers.push(ko.observable(self.numbers().length));
}
self.unwrappedNumbers = ko.pureComputed(function() {
return self.numbers().map(ko.unwrap);
});
self.cleanedNumber = ko.pureComputed(function() {
return self.unwrappedNumbers().join("");
});
};
ko.applyBindings(new NumberField());
label { display: block }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="foreach: numbers">
<label>
<span data-bind="text: 'Number ' + $index()"></span>
<input type="number" data-bind="textInput: $parent.numbers()[$index()]">
</label>
</div>
<button data-bind="click: add">add</button>
<pre>
Unwrapped:<code data-bind="text: unwrappedNumbers"></code>
Cleaned:<code data-bind="text: cleanedNumber"></code>
</pre>

Knockout validation extend 'required only if' with observable parameter

Is there a way to extend an observable to make it required only if and observable is true, and that changes can be tracked?
For example, I got this custom extender:
ko.extenders.requiredOnlyIf = function (target, makeRequired) {
target.HasError = ko.observable();
function validate(newValue) {
if (makeRequired) {
target.HasError(newValue ? false : true);
} else {
target.HasError(false);
}
}
validate(target());
target.subscribe(validate);
return target;
}
And this observables:
self.Field1 = ko.observable().extend({ requiredOnlyIf : self.Field2() });
self.Field2 = ko.observable().extend({ requiredOnlyIf : self.Field1() });
This observables are dependant, so, if one is filled, the other must be filled too. But the extender only works fine when the value is binding the first time, but when the value of any of the observables is changed is not working.
A couple of things:
You need to pass the observables, not the observables' contents, when setting up the extensions. (You can't create Field1 based on Field2 before Field2 exists, either.)
You need two subscriptions, so that if a change to one makes the other invalid, that is noticed.
(update) Rather than subscriptions and an observable, you can use a computed
ko.extenders.requiredOnlyIf = function(target, makeRequired) {
target.HasError = ko.pureComputed(() => {
const otherHasValue = !!makeRequired();
const targetHasValue = !!target();
return otherHasValue && !targetHasValue;
});
return target;
}
self = {};
self.Field1 = ko.observable();
self.Field2 = ko.observable().extend({
requiredOnlyIf: self.Field1
});
self.Field1.extend({
requiredOnlyIf: self.Field2
});
ko.applyBindings(self);
.invalid {
border-color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<input data-bind="value: Field1, css: {invalid: Field1.HasError()}" />
<input data-bind="value: Field2, css: {invalid: Field2.HasError()}" />

Javascript: Empty an input field and press enter

I use knockout framework. I have an observable array that can be filtered using ko.utils.arrayFilter
Now, I want to add a button to reset the array.
As the array gets reset when the input field is emptied using keystrokes, I wanted to simulate it with emptying the input field and then pressing the enter key
self.resetFilter = function (){
$('#filter').val('');
var e = $Event("keypress");
e.which = 13;
$("#filter").trigger(e);
};
Not sure, if the code is wrong. Or if this is a problem with Knockout, as I am not using Knockout to reset.
Below the entire code for the filtering function and the data binding in the HTML
Javascript
self.stringStartsWith = function(string, startsWith) {
string = string || "";
if (startsWith.length > string.length)
return false;
return string.substring(0, startsWith.length) === startsWith;
};
self.filter = ko.observable('');
self.filteredItems = ko.computed(function() {
var filter = self.filter().toLowerCase();
self.resetFilter = function() {
$('#filter').val('');
var e = $Event("keypress");
e.which = 13;
$("#filter").trigger(e);
};
if (!filter) {
return self.venueList();
} else {
return ko.utils.arrayFilter(self.venueList(), function(venue) {
console.log(venue);
return self.stringStartsWith(venue.name.toLowerCase(), filter);
console.log(venue);
});
}
}, self.venueList);
};
HTML
<li>
<input placeholder="Search" id="filter" type="text" data-bind="value: filter, valueUpdate: 'afterkeydown'" autocomplete="off">
<button data-bind="click: function(){resetFilter();}">Reset</button>
</li>
The beauty of knockout is that you can use data-bindings to accomplish what you want to do. While it plays nicely with other libraries like jQuery, you can probably find a more elegant knockout-only solution. You mentioned that you're not using knockout to reset. Is there a reason for that?
Further, you can use the textInput binding instead of using valueUpdate: 'afterkeydown' I'm not sure what your desired result is in terms of the flow of your search, but based on what you've provided in your question I put this example together. Pressing enter resets your filter - I'm not sure if that is your intended behavior as it seems a little strange from a UX perspective, but there it is nonetheless
var ViewModel = function() {
var self = this;
self.filter = ko.observable();
self.list = ko.observableArray([
"fruit",
"bread",
"dad",
"zoo",
"keyboard",
"monkey",
"tiger",
"apple",
"bicycle",
"father",
"mother",
"test",
"computer",
"programming",
"ninja",
"love",
"earth",
"nothing",
"money"
]);
self.filteredList = ko.computed(function() {
return ko.utils.arrayFilter(self.list(), function(item) {
return item.toLowerCase().indexOf(self.filter()) > -1;
});
});
self.clearFilter = function() {
self.filter('');
}
self.onEnter = function(d, e) {
if (e.keyCode === 13) {
//alert("You want to search for: " + self.filter());
self.clearFilter();
}
return true;
}
}
ko.applyBindings(new ViewModel())
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<input type="text" placeholder="Search" data-bind="textInput: filter, event: { keypress: onEnter }" />
<input type="button" data-bind="click: clearFilter" value="Reset" />
<ul data-bind="foreach: filteredList">
<li data-bind="text: $data"></li>
</ul>

KnockoutJS setting focus after adding to array/collection

I have viewmodel with an array of diagnosis codes. In my html I have a button data-bound to a click that adds a blank diagnosis code to the array. This all works.
What I cant figure out, is how to set focus to the dynamically added textbox when a code is added. What can I add :
<h3>Diagnosis Codes<input type="button" value="Add" data-bind="click:AddDiagnosisCode"/></h3>
<div data-bind="foreach:DiagnosisCodes">
<div><input type="text" data-bind="value:$data"/>
</div>
</div>
<script type="text/javascript">
function AddDiagnosisCode(item)
{
item.DiagnosisCodes.push("");
}
var vm = {
"DiagnosisCodes": ["2345","6789"]
};
var viewModel = ko.mapping.fromJS(vm);
ko.applyBindings(viewModel);
</script>
Use the built-in binding hasFocus and set it to true
<input type="text" data-bind="value:$data, hasFocus: true">
See http://jsfiddle.net/eT3Y8/
It can be done with a custom binding. The harder part in this approach is to not focus on the boxes of the elements that are initially in the list. That's why I needed an extra isNew property, which is false for the already existing elements. I also used jquery to focus :) Fiddle: http://jsfiddle.net/hv9Dx/1/
html:
<h3>Diagnosis Codes<input type="button" value="Add" data-bind="click:AddDiagnosisCode"/></h3>
<div data-bind="foreach:DiagnosisCodes">
<div><input type="text" data-bind="value:value, focusOnCreate:isNew()"/>
</div>
</div>
js:
var Record = function(value, isNew){
var self = this;
self.value = ko.observable(value);
self.isNew = ko.observable(isNew || false);
}
var VM = function() {
var self = this;
self.DiagnosisCodes = ko.observableArray([
new Record("2345"),
new Record("6789")]);
self.enableFocus = ko.observable(true);
self.AddDiagnosisCode = function(){
self.DiagnosisCodes.push(new Record("", true));
}
}
ko.bindingHandlers.focusOnCreate = {
init:function(element, valueAccessor, allBindings, viewModel, bindingContext) {
if(valueAccessor()){
$(element).focus();
}
}
}
ko.applyBindings(new VM());

Categories

Resources