Knockout validation extend 'required only if' with observable parameter - javascript

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()}" />

Related

Dynamically initialise multiple input fields in plugin

I've many forms on one page. Each of the forms should have a phone number field. Those fields are driven by JS plug-in.
So I'm getting a big number of fields which should be properly initialized.
If I will do it manually I will get:
forms * phone input fields = number of initializations.
At this moment I only have the very first field working. Other doesn't initialize.
My markup looks like:
<input type="tel" class="phone_flag" name="phone_tab1[main]" required="">
<input type="tel" class="phone_flag" name="phone_tab2[main]" required="">
<input type="tel" class="phone_flag" name="phone_tab3[main]" required="">
xxx
...
I got a piece of advice: in order to make in properly work, I should have querySelectorAll with forEach loop. Then I should call PhoneDisplay function, don't pass the class name, instead pass in the element itself. Afterward, initialize the plugin on that element directly.
I only came to this solution, but it only inits the first element.
JS init code:
document.querySelectorAll('.phone_flag').forEach(el => {
PhoneDisplay(el.className);
});
function PhoneDisplay(ClassName){
var input = document.querySelector('.' + `${ClassName}`);
var iti = window.intlTelInput(input, {
hiddenInput: "full",
initialCountry: "auto",
geoIpLookup: function(callback) {
$.get('proxy.php', function() {}).always(function(resp) {
var countryCode = (resp && resp.country) ? resp.country : "";
callback(countryCode);
});
},
hiddenInput: "full_phone",
utilsScript: "intlTelInput/js/utils.js"
});
var reset = function() {
input.classList.remove("error");
errorMsg.innerHTML = "";
errorMsg.classList.add("hide");
validMsg.classList.add("hide");
};
input.addEventListener('blur', function() {
reset();
if (input.value.trim()) {
if (iti.isValidNumber()) {
validMsg.classList.remove("hide");
} else {
input.classList.add("error");
var errorCode = iti.getValidationError();
errorMsg.innerHTML = errorMap[errorCode];
errorMsg.classList.remove("hide");
}
}
});
input.addEventListener('change', reset);
input.addEventListener('keyup', reset);
}
document.querySelector returns the first query, so var input is always the first input. You should just pass in the element itself in the forEach loop: PhoneDisplay(el); and then function PhoneDisplay(input) and remove the 'var input=' line.
jQuery(document).ready(function($) {
var input = $("input[name=phone]");
input.each(function() {
intlTelInput($(this)[0], {
initialCountry: "auto",
nationalMode: false,
separateDialCode: true,
preferredCountries: ["ua", "pl", "us"],
geoIpLookup: function(success, failure) {
$.get("https://ipinfo.io", function() {}, "jsonp").always(function(resp) {
var countryCode = (resp && resp.country) ? resp.country : "us";
success(countryCode);
});
},
});
});
});

ICheck plugin not working with sublist in Knockout.js

I have a Knockout model containing a bool observable and a list of objects that contain a bool observable.
I have custom binding iCheckedSys, for the bool in the model, to work with iCheck, and custom binding iCheckedPrimary to work with iCheck on the bool in the objects in the model's list.
iCheckedSys functions correctly, and valueAccessor() returns observable.
However in the list, valueAccessor() returns false in iCheckedPrimary.
If I just use checkbox for the list objects, it works just fine.
How can I have iCheck work with the list objects?
Thanks much.
<div class="form-horizontal no-margin form-border" data-bind="UserViewModel">
<label>
<input type="checkbox" data-bind="iChecked: IsSysAdmin">
</label>
<tbody data-bind="foreach: RolesList">
<td><input type="checkbox" data-bind="checked: IsPrimary" /></td>
</tbody>
</div>
<script>
var Role = function () {
var self = this;
self.ID = ko.observable();
self.IsPrimary = ko.observable();
};
var UserViewModel = function () {
var self = this;
self.ID = ko.observable(#Html.Raw(Json.Encode(Model.ID)));
self.IsSysAdmin = ko.observable(#Html.Raw(Json.Encode(Model.IsSysAdmin)));
self.RolesList = ko.observableArray();
ko.bindingHandlers.iChecked = {
init: function (element, valueAccessor) {
$(element).iCheck({
checkboxClass: "icheckbox_minimal-green",
radioClass: "iradio_minimal-green", increaseArea: "20%"
});
$(element).on('ifChanged', function () {
var observable = valueAccessor();
observable($(element)[0].checked);
});
},
update: function (element, valueAccessor) {
var value = ko.unwrap(valueAccessor());
if (value) {
$(element).iCheck('check');
} else {
$(element).iCheck('uncheck');
}
}
};
};
var viewModel = new UserViewModel();
ko.applyBindings(viewModel);
</script>
I realized that regardless of with record I changed iCheckedPri in, only the last record in viewModel.RolesList was being passed in valueAccessor. So the current record and it's value wasn't being accessed.
I don't know if this is the proper way...
but instead of the value, I passed iCheck with the record object itself and updated it with the current checkbox value, and updated iCheck (check/uncheck) with the object value.
<td><input type="checkbox" data-bind="iCheckedPri: $rawData"></td>
ko.bindingHandlers.iCheckedPri = {
$(element).on('ifChanged', function (event) {
var observable = valueAccessor();
observable = $(element)[0].checked;
valueAccessor().IsPrimary = this.checked;
});
},
update: function (element, valueAccessor) {
var value = ko.unwrap(valueAccessor());
if (value.IsPrimary) {
$(element).iCheck('check');
} else {
$(element).iCheck('uncheck');
}
}
};

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>

2 states roundable numeric text box with knockoutjs

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"/>

Call Knockout Validation Extender on Button Click

I've been reading many questions that ask how to call a knockout validation extender on a button click event. But all the answers that come close to answering the question, involve workarounds using the knockout-validate library. I'm not using the knockout-validate library. All I want to do is validate an input field on a button click event using the validation rules defined in a knockout extender.
For simplicity I'm going to use the required extender from the knockout documentation and apply it to a simple use case. This use case takes an input and on a button click event does something with the value the user entered. Or updates the view to show the validation message if no value was entered.
Knockout Code:
ko.extenders.required = function (target, overrideMessage) {
target.hasError = ko.observable();
target.validationMessage = ko.observable();
function validate(value) {
target.hasError(value ? false : true);
target.validationMessage(value ? "" : overrideMessage || 'Value required');
}
return target;
};
function MyViewModel() {
self = this;
self.userInput = ko.observable().extend({ required: 'Please enter a value' });
/**
* Event handler for the button click event
*/
self.processInput = function () {
//Validate input field
//How to call the validate function in the required extender?
//If passes validation, do something with the input value
doSomething(self.userInput());
};
}
Markup:
<input type="text" data-bind="value: userInput, valueUpdate: 'afterkeydown'" />
<span data-bind="visible: userInput .hasError, text: userInput .validationMessage" class="text-red"></span>
<button data-bind="click: processInput ">Do Something</button>
How can I trigger the validation on the button click event and show the validation message if it doesn't pass validation?
I believe you were looking at the example here - http://knockoutjs.com/documentation/extenders.html
You can not call validate directly, but the subscribe watches the value and runs the validate function on change and updates an observable you can check - hasError.
ko.extenders.required = function (target, overrideMessage) {
target.hasError = ko.observable();
target.validationMessage = ko.observable();
function validate(value) {
target.hasError(value ? false : true);
target.validationMessage(value ? "" : overrideMessage || 'Value required');
}
//initial validation
validate(target());
//validate whenever the value changes
target.subscribe(validate);
//return the original observable
return target;
};
function MyViewModel() {
self = this;
self.userInput = ko.observable().extend({ required: 'Please enter a value' });
/**
* Event handler for the button click event
*/
self.processInput = function () {
if(self.userInput.hasError()){
console.log('has error');
}else{
console.log('no error');
}
};
}
// Activates knockout.js
ko.applyBindings(new MyViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<input type="text" data-bind="value: userInput, valueUpdate: 'afterkeydown'" />
<span data-bind="visible: userInput .hasError, text: userInput .validationMessage" class="text-red"></span>
<button data-bind="click: processInput ">Do Something</button>

Categories

Resources