Knockout custom handler to hide textbox value if it is disabled - javascript

I have a form that basically has a handful of properties that are shared between a few items. When you select the radio button for the item the text boxes enable for data entry, only one item can be selected at a time.
I have everything setup and working except I do not want the bound values to display in the textbox if the control is disabled. I have been trying to work with the handlers but I am having a hell of a time trying to understand how to make things work the way I need. I have looked at many articles by Ryan and the custom handlers he has provided but I need an epiphany, but until then I am seeking your help. Also, is there a more appropriate way to handle the IsEnabled function I have created or is that the best way?
Here is the JSFiddle
Updated JSFiddle, instead of doing the value I am attempting to create a custom handler that disabled and deletes the value. It kinda works but it stops after a few updates and the value doesn't get updated.
Here is some sample HTML:
<ul>
<li>
<input type="radio" name="item" value="1" data-bind="checked:Selected" /> Item 1 <input type="text" data-bind="value:Price, enable:IsEnabled('1')" />
</li>
<li>
<input type="radio" name="item" value="2" data-bind="checked:Selected" /> Item 2 <input type="text" data-bind="value:Price, enable:IsEnabled('2')" />
</li>
<li>
<input type="radio" name="item" value="3" data-bind="checked:Selected" /> Item 3 <input type="text" data-bind="enabledValue:Price, enable:IsEnabled('3')" />
</li>
<li>
<input type="radio" name="item" value="4" data-bind="checked:Selected" /> Item 4 <input type="text" data-bind="enabledValue:Price, enable:IsEnabled('4')" />
</li>
</ul>
Here is the sample JS:
var vm = {
Selected: ko.observable('1'),
Price: ko.observable(12),
IsEnabled: function(item){
var selected = this.Selected();
return (selected == item)
}
}
ko.applyBindings(vm);
(function (ko, handlers, unwrap, extend) {
"use strict";
extend(handlers, {
enabledValue: {
init: function (element, valueAccessor, allBindings) {
var bindings = allBindings();
var enabled = ko.unwrap(bindings.enable);
var value = unwrap(valueAccessor());
if (enabled)
handlers.value.init();
},
update: function (element, valueAccessor, allBindings) {
var bindings = allBindings();
var enabled = ko.unwrap(bindings.enable);
var value = unwrap(valueAccessor());
handlers.value.update(element,function() {
if(enabled)
return valueAccessor(value);
});
}
}
});
}(ko, ko.bindingHandlers, ko.utils.unwrapObservable, ko.utils.extend));

Tony. I've just simplified your sample and got it working with sharing same value property between different items. The main idea that a binding will store internal computed and will bind an element against it.
extend(handlers, {
enableValue: {
init: function (element, valueAccessor, allBindings) {
var showValue = ko.computed({
read: function(){
if (unwrap(allBindings().enable)) {
return unwrap(valueAccessor());
} else {
return '';
}
},
write: valueAccessor()
});
ko.applyBindingsToNode(element, { value: showValue });
}
}
});
http://jsfiddle.net/7w566pt9/4/
Note that in KO 3.0 ko.applyBindingsToNode is renamed to ko.applyBindingAccessorsToNode.
But wouldn't it have more sense to make the bindings remember last entered value for each item? It's quite simple to implement.
Update
Remembering last edited value for the particular item is similar in the manner that you should keep that value internally like showValue. Let's name it lastValue:
extend(handlers, {
enableValue: {
init: function (element, valueAccessor, allBindings) {
// Create observable `lastValue` with some default content.
// It will be created for EVERY binding separately.
var lastValue = ko.observable(0);
// If an item is currently enabled then set `lastValue` to the actual value.
if (unwrap(allBindings().enable)) lastValue(unwrap(valueAccessor()));
// This piece will be executed only once (for the selected item) and other
// items will store default value in `lastValue`!
// It's the internal anonymous computed intended to update bound
// price to reflect currently edited value.
ko.computed(function(){
if (unwrap(allBindings().enable)) valueAccessor()(lastValue());
});
// Note that passed function will be triggered whenever item is enabled
// and/or `lastValue` changes.
// Here we just change valueAccessor() to `lastValue`.
var showValue = ko.computed({
read: function(){
if (unwrap(allBindings().enable)) {
return lastValue();
} else {
return '';
}
},
write: lastValue
});
ko.applyBindingsToNode(element, { value: showValue });
}
}
});
http://jsfiddle.net/7w566pt9/8/
I hope it is nearly what you expected. Usually in such cases the real problem is not implementing a feature but describing how the feature should work.

Since my additions to the answer have been edited out I have added this answer to help those new to KO.
Here is a KO 3.0 implementation using ko.applyBindingAccessorsToNode.
extend(handlers, {
enableValue: {
init: function (element, valueAccessor, allBindings) {
var showValue = ko.computed({
read: function () {
if (unwrap(allBindings().enable)) {
return valueAccessor(); // CHANGED
} else {
return '';
}
},
write: valueAccessor //CHANGED
});
ko.applyBindingAccessorsToNode(element, { value: showValue }); //CHANGED
}
}
});
As stated in the release notes there is no official documentation for it yet but this is what I was able to put together. I used the group message to determine the differences. Hopefully this will save someone time until it has more documentation.
Release Notes
ko.applyBindingsToNode is superseded by
ko.applyBindingAccessorsToNode. The second parameter takes an object
with pairs of bindings and value-accessors (functions that return the
binding value). It can also take a function that returns such an
object. (This interface isn't currently documented on the website.)
Group Message from Michael Best stating it is better.
Compatibility Issue

Related

KnockoutJS Binding a checkbox with both "checked" and "click" bindings causes unexpected behavior

here is the code I've been working on.
Javascript:
function ViewModel() {
var self = this;
self.isChecked = ko.observable(false);
self.testing = function(){
console.log("hello from testing");
}
}
var app = new ViewModel();
ko.applyBindings(app);
And here's the html:
<div>
<div>
<button data-bind="click: testing" type="button">Something</button>
<input data-bind="checked: isChecked, click: testing" type="checkbox" />
<input data-bind="checked: isChecked" type="checkbox" />
</div>
</div>
What I'm looking to accomplish is that I want a checkbox, whose value is data-binded to a variable in my model and updates accordingly. And at the same time, whenever a user clicks the checkbox to change its boolean value, I want a function to be executed AFTER the value is changed in the model and the checkbox is updated.
I have two buttons data-binded to the same value just for testing purposes. When I click the checkbox that is binded with click to testing, its checkmark doesn't change, but the function executes correctly. However, the other checkbox DOES indeed update to reflect the changes in the model when the button is clicked.
What is happening that causes this behavior, and how could I write a better solution to achieve what I'm looking for?
EDIT: So apparently, by adding a return true; to the function, I managed to... arrive at the intended behavior?
function ViewModel() {
var self = this;
self.isChecked = ko.observable(false);
self.testing = function(){
console.log(self.isChecked());
console.log("hello from testing");
console.log(self.isChecked());
return true;
}
}
var app = new ViewModel();
ko.applyBindings(app);
The question is then, why does this happen, and is it safe?
This seems like an XY problem. So, in addition to what #Tomalak mentioned, if you want to trigger a function when the checkbox value is changed, you can subscribe to the isChecked observable
function ViewModel() {
var self = this;
self.isChecked = ko.observable(false);
self.isChecked.subscribe(function(newValue) {
// this gets executed every time "isChecked" changes
console.log(self.isChecked());
console.log(newValue);
});
}
ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<input data-bind="checked: isChecked" type="checkbox" />
The advantage of using subscribe over click binding is that, this function gets triggered even when you set the value of checkbox programmatically like this self.isChecked(false)

How to pass an argument from select options to trigger and filter array observables using foreach binding

In the markup below, I am binding unique data containing a list of Continents: I am also subscribing to the selected value and triggering an event with the continent selected by the user.
<div id="country-collection">
<select data-bind="options: UniqueContinent,
value: SelectedContinent"></select>
</div>
code:
self.CountryData = ko.observableArray([]);
self.SelectedContinent = ko.observable('');
self.UniqueContinent = ko.dependentObservable(function() {
var continent = ko.utils.arrayMap(self.CountryData(),
function(item){
return item.Continent
})
return ko.utils.arrayGetDistinctValues(continent).sort();
});
The below function is fired each time a selection is made:
self.SelectedContinent.subscribe(function (selectedValue) {
// alert(selectedValue);
});
Using the code above, I need to populate the following list with all of the countries based on the default Continent onload or a selected Continent: So if Asia is selected, the only countries displayed are countries in Asia and their respective details.
<div id="country-list">
<ul data-bind= "foreach: CountryData">
<li><span data-bind="text: Country"></span></li>
// More list stuff here (removed for brevity)
</ul>
</div>
I tried this but it only worked if the value is hard coded. I need the Countries to load based on the default value or selected value of the select options:
self.SelectedContinent.subscribe(function (selectedValue) {
// Call this function when changes are made
self.FilteredEntries = ko.computed(function() {
return ko.utils.arrayFilter(self.CountryData(), function(item) {
// I need to use the selected value
return item.Continent === 'SOUTH AMERICA';
});
});
});
You can remove the subscribe function:
// Call this function when changes are made
self.FilteredEntries = ko.computed(function() {
return ko.utils.arrayFilter(self.CountryData(), function(item) {
// I need to use the selected value
return item.Continent === self.SelectedContinent();
});
});
With subscribe you are creating a new computed observable each time the selection changes and the reassigned computed observables are never bound to the DOM.
You can check out the working demo in this fiddle.
You can use the awsome Knockout Projections plugin, available here; knockout-projections.
Defining the filtered array observable is very simple, and the implementation is highly efficient:
var FilteredCountries = self.CountryData().filter(
function(c) { return c.Continent === self.SelectedContinent();
});

Save chrome extension options to chrome.storage?

I'm making a chrome extension that has an options.html page.
On the options page I have 3 checkboxes. When one of the checkboxes is checked/unchecked the value of that checkbox is saved to- or removed from chrome.storage using chrome.storage.sync.set(). But I can't seem to retrieve the data once it is saved using chrome.storage.sync.get().
Here's the code I have:
options.html:
<div class="checkboxes">
<input id="test1" name="test1" type="checkbox">
<input id="test2" name="test2" type="checkbox">
<input id="test3" name="test3" type="checkbox">
</div>
options.js:
$(document).ready(function() {
var storage = chrome.storage.sync;
//Retrieve existing settings
$(':checkbox').each(function() {
$(this).prop('checked', function() {
var name = this.name;
storage.get(name, function(test){
console.log(test[name]);
});
});
});
$(".checkboxes").on("change", ":checkbox", saveSettings);
//Save or delete settings
function saveSettings() {
var name = this.name;
if($(this).is(':checked')) {
storage.set({name:'checked'},function(){
console.log("saved");
});
}
else {
storage.remove(name, function(){
console.log("removed");
});
}
}
});
Above outputs:
console.log(test[name]); > undefined
console.log("saved"); > saved
console.log("removed"); > removed
Why do I get "undefined"?
I tired the same thing using localStorage which worked fine on only option.js. But when I tried to retrieve the stored data on background.js it didn't work. I figured their localStorage details are not accessible to each other.
I should also mention I'm not the best at javascript/jquery and I'm still learning, so please excuse my mistakes.
You've got two main issues:
chrome.storage.sync.get is asynchronous, while the function in jQuery.fn.prop(propertyName, function(index, oldPropertyValue) ) must synchronously return the desired value.
storage.set({name:'checked'}, ...); will create a property called "name" with value "checked", not a property with the name as specified in the name variable with value "checked".
To solve the first problem, swap the order of operations: First read the preference, then set the property:
//Retrieve existing settings
$(':checkbox').each(function(index, element) {
var name = this.name;
storage.get(name, function(items) {
element.checked = items[name]; // true OR false / undefined (=false)
});
});
To solve the second issue, first create an object, assign the new property, and save the result. Note: Since a checkbox can only have two states, I recommend to not save the string "checked", but use a boolean (true / false):
function saveSettings() {
var name = this.name;
var items = {};
items[name] = this.checked;
storage.set(items, function() {
console.log("saved");
});
}
PS. When you're certain that you're dealing with checkboxes, use element.checked instead of $(element).is(':checked'). The former is shorter and faster. Using jQuery adds no value here. See also: When to use Vanilla JavaScript vs. jQuery?

Using checkedValue binding with radio buttons

I'm trying to make use of the checkedValue binding introduced in knockout version 3, with radio buttons , but am not getting the behavior I expect.
Here's an example: (the viewModel has two properties; list is an array; checkedVal is an observable)
<div data-bind="foreach:list">
<input type="radio" data-bind="
checkedValue: {
data: $data,
index: $index(),
},
checked: $parent.checkedVal
"/>
<span data-bind="text: $data"></span>
</div>
JSFiddle
I expect the radio buttons to behave normally, and checkedVal to be an object containing the data and index. checkedVal is as I expect, but the radio buttons don't select. Oddly, in my actual code the behavior is inconsistent; sometimes the radio butttons work and sometimes they don't, but it consistently doesn't work in the fiddle, as far as I can see.
Is this a bug, or am I misunderstanding how this should be working?
Your checkedValue binding becomes a function as follows:
function () {
return {
data: $data,
index: $index(),
};
}
Each time the checked binding updates, it calls this function to get the value. But the function always returns a new object. Even though the objects contains the same data, Knockout doesn't see them as the same.
You can solve this by making the value a string.
<input type="radio" data-bind="
checkedValue: JSON.stringify({
data: $data,
index: $index(),
}),
checked: $parent.checkedVal
"/>
Or by binding to a consistent value.
<input type="radio" data-bind="
checkedValue: $data,
checked: $parent.checkedVal
"/>
EDIT:
You can use a custom binding that follows the same pattern as checked, but allows for a comparison function.
ko.bindingHandlers.radioChecked = {
init: function (element, valueAccessor, allBindings) {
ko.utils.registerEventHandler(element, "click", function() {
if (element.checked) {
var observable = valueAccessor(),
checkedValue = allBindings.get('checkedValue');
observable(checkedValue);
}
});
},
update: function (element, valueAccessor, allBindings) {
var modelValue = valueAccessor()(),
checkedValue = allBindings.get('checkedValue'),
comparer = allBindings.get('checkedComparer');
element.checked = comparer(modelValue, checkedValue);
}
};
Then objects can be compared by contents.
this.itemCompare = function(a, b) {
return JSON.stringify(a) == JSON.stringify(b);
}
jsFiddle: http://jsfiddle.net/mbest/Q4LSQ/
It appears that this issue has been resolved in newer versions of KO. As of version 3.2 I'm no longer seeing this behavior mentioned in my original question.
Here's a working JSFiddle, identical to the original, except with KO updated to 3.2 .

Knockout JS - data-bind multiple values

Question about JS Knockout library - I have three inputs, all data-Bind-ed to the same variable. Two have a boolean value of false and one has a boolean value of true. (I can't change them to ints, unfortunately, which would make this problem easier). Although the two false-valued inputs share behavior, I need to differentiate between them somehow to trigger slightly different behaviors.
Is it possible to data-bind each to another variable, with different values? So instead of each being
<input data-Bind="checked:test" value="false">
I would have something like
<input data-Bind="test, test2" value="false, 1">
and
<input data-Bind="test, test2" value="false, 2">?
I tried that directly and didn't work so I don't know if it's possible. Thanks so much.
You cant bind multiple variables directly but creating a custom bind function do the trick for you.
Example : http://jsfiddle.net/gurkavcu/ePW8Y/
** Change input value (true , false) to trigger the update function
HTML
<input data-bind="customData: test , v1 : test2"/>
<div>
<span data-bind ="text : test"/>
</div>
<div>
<span data-bind ="text : test2"/>
</div>
JS
ko.bindingHandlers.customData = {
init: function(element, valueAccessor, allBindingsAccessor, viewModel) {
$(element).change(function () {
valueAccessor()(element.value);
});
},
update: function(element, valueAccessor, allBindingsAccessor, viewModel) {
var value =ko.utils.unwrapObservable(valueAccessor());
var v1 = allBindingsAccessor().v1;
if(value === "true") {
v1("1");
console.log(v1());
}
else if(value === "false") {
v1("2");
console.log(v1());
}
}
};
function ViewModel() {
this.test = ko.observable(false);
this.test2 = ko.observable("2");
};
$(function() {
var viewModel = new ViewModel();
ko.applyBindings(viewModel);
})​
Modify the update function for your needs. You can add any number of variable to the binding with v1 : ... , v2 : ... , v3 : ... and access it via allBindingsAccessor().v1 , allBindingsAccessor().v2 , allBindingsAccessor().v3

Categories

Resources