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 .
Related
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
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?
I'm trying to build a view that will initially display text. If the user double-clicks, it will replace that text with an input field. This way the user can easily update the text (like using the "contenteditable" attribute).
I have an approach that works in Ember pre4, but not in Ember RC1. In RC1, the Ember.TextField does not initialize to the parent view's value property. When you double-click the label text, it creates an empty input field. Here are two fiddles:
Pre4 (working): http://jsfiddle.net/mattsonic/cq5yy/5
RC1 (same code - not working): http://jsfiddle.net/mattsonic/UUac9/15
Any idea what changed inside Ember? Thanks.
Here is the code:
App.InputView = Ember.TextField.extend({
classNames: ["input-small"],
valueBinding: "parentView.value",
didInsertElement: function () {
this.$().focus()
},
focusOut: function () {
parent = this.get("parentView");
parent.setLabelView();
}
});
App.LabelView = Ember.View.extend({
tagName: "span",
template: Ember.Handlebars.compile("{{view.value}}"),
valueBinding: "parentView.value",
doubleClick: function () {
parent = this.get("parentView");
parent.setInputView();
}
});
App.LabelEditView = Ember.ContainerView.extend({
tagName: "span",
labelView: App.LabelView.extend(),
inputView: App.InputView.extend(),
didInsertElement: function () {
this.setLabelView();
},
setInputView: function () {
this.set("currentView", this.get("inputView").create());
},
setLabelView: function () {
this.set("currentView", this.get("labelView").create());
}
});
I found a solution that I don't like at all. But, it solves the problem as described.
focusIn: function() {
var val = this.get("parentView.value");
this.set("value", "");
this.set("value", val);
},
If you set the input field's value to the correct value during the focusIn event, it still fails. But, if you set the input field's value to a different value and then switch it back, the input field will appear with the correct value.
I would love to know a better way to solve this problem. The Ember pre4 solution is more much elegant than this.
Working fiddle: http://jsfiddle.net/mattsonic/UUac9/19/
I'm using AngularJs on my project and i've a property on my viewModel that is connected to a dropdown (< select >)
that dropdown have a empty value witch is selected by default, what i want is to prevent user to select that empty value after he select some other value.
ive started to look to $watch, but i dont know if there is some way to cancel the "changing oof that property", some thing like this:
$scope.$watch('myProp', function (newVal, oldVal, scope) {
if (newVal) { scope.preventDefault(); }
}
any idea, this is the base idea, on a more advanced development i need to ask users for a confirmation.
any ideas?
what i want is to prevent user to select that empty value after he select some other value
This should happen automatically for you, as long as you don't assign the ng-model property a value initially. So using the <select> shown below, don't initialize $scope.selected_year in your controller:
<select ng-model="selected_year" ng-options="year for year in years"></select>
When the list displays initially, Angular will have added an option like this to the HTML, since $scope.selected_year is not currently set to a valid option/value:
<option value="?" selected="selected"></option>
After selecting a valid choice, that option will magically disappear, so the user will not be able to select it again. Try it in this fiddle.
If the ng-model property already has a valid value assigned when the select list is first displayed, then you can assign a controller function to the undocumented ng-change parameter:
<select ... ng-change="preventUserFromDoingXzy()">
Inside function preventUserFromDoingXzy() you can do what you need to do to control what the user can select, or modify the model.
You can just add ng-required to the select.
If there is no initial value to the model then an empty option will be added and on change to a valid value it will remove the empty option
EDITED jsFiddle to revert to previous value and to include the ng-change directive.
From the docs:
The expression is not evaluated when the value change is coming from the model.
This is useful in not interfering with change listeners and creating an infinite loop when reverting the old value in the $apply function
Controller
$scope.options = [{value: 'abc'},{value: 'def'}];
var confirmDialog = function(newVal, yes, no) {
// obviously not a good way to ask for the user to confirm
// replace this with a non blocking dialog
//the timeout is only for the confirm since it's blocking the angular $digest
setTimeout(function() {
c = confirm('Is it ok? [' + newVal.value + ']');
if(c) {
yes();
}
else {
no();
}
}, 0);
};
//Asking for confirmation example
function Ctrl($scope) {
$scope.options = [{value: 'abc'},{value: 'def'}];
$scope.select = undefined;
var oldSelect = undefined;
$scope.confirmChange = function(select) {
if(oldSelect) {
confirmDialog(select,
function() {
oldSelect = select;
},
function() {
$scope.$apply(function() {$scope.select = oldSelect;});
});
}
else {
oldSelect = select;
}
}
}
Template
<div ng-controller="Ctrl">
<select ng-model="select" ng-options="o.value for o in options"
ng-required ng-change="confirmChange(select)">
</select>
</div>
Probably the easiest, cleanest thing to do would be adding an initial option and setting disabled on it:
<option value="?" selected="selected" disabled></option>
Actually, it is easier to remove the empty value. Suppose you have a list of options:
$scope.options = [{value: ''}, {value: 'abc'},{value: 'def'}];
and a select:
<select ng-model="select" ng-options="o.value for o in options"></select>
Then $watch the model:
$scope.$watch('select', function(value) {
if (value && value !== '') {
if ($scope.options[0].value === '') {
$scope.options = $scope.options.slice(1);
}
}
}, true);
See it in action here.
PS Don't forget the objectEquality parameter in the $watch or it won't work!
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