I have a select control bound to a Knockout observable array:
<select data-bind="event: { change: selectedProductOfferingChange }, options: $parent.productTypes, optionsText: 'text', optionsCaption: '-- Select --', value: selectedProductType, enable: !isReadOnly()"></select>
When the selection is changed, I want to run some code, perhaps make an AJAX call. If the change is not allowed, I want to cancel the change and display a modal dialog. I can't subscribe to the property as that will fire after the change has taken place. I would need the new value to determine if the change should be cancelled or not.
I tried the following in the viewmodel but the change is not cancelled though the property (selectedProductOffering) on the viewmodel is not updated:
self.selectedProductOfferingChange = function (data, event) {
event.stopImmediatePropagation();
return false;
};
Could I use the "beforeChange" option with subscribe?
self.selectedProductType.subscribe(function (previous) {
}, self, "beforeChange");
Can the change be cancelled here?
After some thought, here's what I came up with:
self.selectedProductOfferingChange = function (data, e) {
// Do any checking here
if (confirm("OK to make this change ?")) { return; }
// This stops the viewmodel property from being updated
e.stopImmediatePropagation();
// Since the viewmodel property hasn't changed, force the view to update
self.selectedProductType.valueHasMutated();
};
The problem with this code is that it doesn't give you access to the new value. A computed will solve this issue:
<select data-bind="options: $parent.productTypes, optionsText: 'text', optionsCaption: '-- Select --', value: computedSelectedProductType, enable: !isReadOnly()"></select>
self.computedSelectedProductType = ko.computed({
read: function () {
return self.selectedProductType();
},
write: function (value) {
// Do any checks here. If you want to revert to the previous
// value, don't call the following but do call:
// self.selectedProductType.valueHasMutated()
self.selectedProductType(value);
},
owner: self
});
Related
I'm trying to implement a select that calls a function even if the same option is selected twice. Following one of the answers on this thread, I've set the selectedIndex to -1 on focus. However, I'm still not getting the function call when the same option is selected twice.
var scaleSelect = new ComboBox({
id: "scale_select",
style: {width: "150px"},
name: "scale_select",
placeHolder: "Choisir une échelle",
store: scaleStore,
disabled: true,
onFocus: function() {
this.selectedIndex = -1;
console.log(this.selectedIndex); //-1
},
onChange: function(value){
mapScale = value;
window.myMap.setScale(mapScale);
}
}, "scale_select");
scaleSelect.startup();
UPDATE Attempting to set the selected index within onChange still doesn't call the function--wondering if this has to do with the fact that selected index is undefined onChange..
var scaleSelect = new ComboBox({
id: "scale_select",
style: {width: "150px"},
name: "scale_select",
placeHolder: "Choisir une échelle",
store: scaleStore,
disabled: true,
onChange: function(value){
mapScale = value;
window.myMap.setScale(mapScale);
var mySelect = document.getElementById("scale_select");
console.log(this.selectedIndex) //undefined
this.selectedIndex = -1;
mySelect.selectedIndex = -1;
console.log(this.selectedIndex); //-1
console.log(mySelect.selectedIndex); //-1
}
}, "scale_select");
scaleSelect.startup();
I tested every things that i knew with no success. I wondering about the thread that you linked and answers with upvotes! I the reason is that a selected option is not an option any more. But i have a suggestion:
Create your own custom Select
It is only a textbox and a div under that with some links line by line.
So I think the problem is that after you've picked a value in the select, it doesn't lose focus. So your onFocus handler won't get called unless the user actually clicks somewhere else on the page. If she doesn't do that, then the select's value won't fire the onChange if she selects the same value again.
So instead of setting the selected index on focus, why not do it in onChange, after you've handled the change? That way your select will be primed for another selection as soon as you're done treating the current one.
UPDATE
Ok, so looking at the Dojo code, this is the best I could come up with:
var scaleSelect = new ComboBox({
id: "scale_select",
style: {width: "150px"},
name: "scale_select",
placeHolder: "Choisir une échelle",
store: scaleStore,
disabled: true,
onChange: function(value){
if(value) { // the .reset() call below will fire the onChange handler again, but this time with a null value
mapScale = value;
window.myMap.setScale(mapScale);
console.log("Changed to", value, this);
this.reset(); // This is the magic here: reset so you are guaranteed to isseu an "onChange" the next time the user picks something
}
}
}, "scale_select");
scaleSelect.startup();
Note that you need to start the whole thing with no value - or else that initial value won't issue an "onChange" event, and every time you reset the widget, it'll go back to the initial value...
Hope this helps!
Within my model I have an object array with observable elements. I have a subscribe on one of these elements (a dropdown list) in order to fill in the next dropdown (cascading dropdowns). I now need to get the description of the selected value in these dropdowns. How would I pull that value from the dropdown list? I am trying to avoid running a query against the server if possible.
In the past I have done self.deptName($('#AssignedDepartment option:selected').text()); but now this dropdown isn't unique. I could have many to choose from. I am using uniqueName: true on each of the fields. How, within the subscribe, do I determine which dropdown changed?
Here is what my subscribe looks like:
// Build the hours types when the request type is selected
self.RequestType.subscribe(function (newValue) {
FindRequestType(newValue, function (record) {
// Do stuff
});
});
Here is the HTML for the dropdown:
<select class="required span12" data-bind="leaveTypeDropDown: RequestType, uniqueName: true"></select>
Here is the bindingHandler:
ko.bindingHandlers.leaveTypeDropDown = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
// If we don't have the leave request dropdown data, pull it.
if (jsonRequestType === "") {
$.ajax({
'async': false,
'url': '/api/payroll/LeaveRequestTypes/',
'dataType': 'text json',
'type': 'GET',
'success': function (json) {
jsonRequestType = json;
jsonRequestTypeRecieved = true;
}
});
}
// Fill the drop down list.
LoadRequestType(element);
// register event handler where bound observable will be changed
ko.utils.registerEventHandler(element, 'change', function () {
var observable = valueAccessor();
observable(element.value);
});
// Select the value from the previous record if there is one.
if (selectedRequestType !== null) {
element.value = selectedRequestType;
ko.utils.triggerEvent(element, 'change');
selectedRequestType = null;
}
}
};
Update 1: Since there is confusion. If I select the value "VAC" with the text of "Vacation" in my dropdown list. How do I get the text "Vacation" into my model?
Update 2: Here is a jsFiddle with my (mostly) working code as listed above. http://jsfiddle.net/MikeWills/cykuqxto/6/ For some reason, on load my drop downs don't work, but add another day and you'll see my working dropdowns.
We have to clean up your approach a little bit. First, let's make all the observables we need on your model and populate the first drop down choices with your ajax call:
var viewModel = {
firstDropDownOptions: ko.observableArray(),
firstDropDownChoice: ko.observable()
};
$.ajax(...).done(function (data) {
// process data and assign
// for the binding below to work, processedData has to have items like this:
// {textProperty: 'some text', otherProperty: ...}
// as you said in a comment, you have option groups so I'm going to assume
// processedData looks like this overall:
// [{group: 'one', options: [{text: 'Vacation', value: 'VAC'}, ...]}, ...]
viewModel.firstDropDownOptions(processedData);
});
Then let's bind that observable to your drop down using the options binding:
<select data-bind="foreach: firstDropDownOptions, value: firstDropDownChoice">
<optgroup data-bind="attr: {label: label}, foreach: options">
<option data-bind="text: text, option: $data"></option>
</optgroup>
</select>
This is using the custom binding from KnockoutJS - Binding value of select with optgroup and javascript objects:
ko.bindingHandlers.option = {
update: function(element, valueAccessor) {
var value = ko.utils.unwrapObservable(valueAccessor());
ko.selectExtensions.writeValue(element, value); // note this is magic
}
};
Finally, let's subscribe to the observable that monitors changes to this first drop down:
viewModel.firstDropDownChoice.subscribe(function (newChoice) {
// in this function, newChoice is the option selected in the first drop down,
// but not just the value, the whole object, so you can do:
console.log(newChoice, newChoice.text);
});
I'm having an issue with a data-bind updating through a custom event that's called when a select element is changed. The idea is that the select element changes which updates the datatype parameter. Then the change event kicks in which updates sliderType which is a boolean and changes to true if a specific value is selected. That all works fine, however the div -data-bind="if: sliderType- does not update on a change, but it works on the page load.
Is there some way to force the page to recheck bindings?
Here is a fragment of the code to give you an idea of my structure.
HTML
<select data-bind="options: Items, optionsText: 'type', value: dataType, optionsCaption: 'Choose Data Item...', event: { change: dataTypeChange }"></select>
<div data-bind="if: sliderType">Here is a message. Astonishing.</div>
JavaScript
dataTypeChange: function() {this.sliderType = (this.dataType._latestValue.id == 6) ? true : false;
ko.observable being used:
{
dataItemId: -1,
name: ko.observable(res.Text),
dataType: ko.observable(res.DataType),
dataTypeChange: function() {
this.sliderType = (this.dataType._latestValue.id == 6) ? true : false;
},
sliderType: sliderActive
}
Stupid mistake on my part, it would help if the data-bind object was an observable
{
dataItemId: -1,
name: ko.observable(res.Text),
dataType: ko.observable(res.DataType),
dataTypeChange: function() {
this.sliderType = (this.dataType._latestValue.id == 6) ? true : false;
},
sliderType: ko.observable(sliderActive)
}
I use select2 and knockoutJs with this simple binding:
ko.bindingHandlers.select2 = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var options = ko.toJS(valueAccessor()) || {};
setTimeout(function () {
$(element).select2(options);
}, 0);
}
};
Markup:
<select class="select2" style="width:100%" data-bind="optionsCaption: '',options: $root.items,optionsText: 'description',optionsValue: 'id', value: $root.selectedItem,select2: { placeholder: 'Select an item...',allowClear: true }"></select>
it works! Now I enabled the allowClear option in Select2 to clear dropdown to a placeholder value like Select an item....
If I click the x icon dropdown properly sets the placeholder but knockout don't update observable binded value!
I think I've to change custombinding adding something like this:
setTimeout(function () {
$(element).select2(options).on("select2-removed", function (e) {
ko.bindingHandlers.value.update(element, function () { return ''; });
});
...
but it won't work!
There are couple of issues.
1) the update in bindinghandler is to update DOM based on value change, you should never define an update callback with the ability to mutate your model.
The right way is, when define a new bindinghandler, use init callback to hook up all change events with the model, the update callback is merely a DOM drawing routine.
If your init provided DOM drawing (such as provided by select2), you don't need to define an update callback.
Hence the line ko.bindingHandlers.value.update(element, function () { return ''; }); only redraw the DOM, it doesn't do what you want.
2) the select2 binding you created has some holes.
first, value binding doesn't know the existence of select2 binding, that's where you struggled.
second, your select2 binding has to wait for other binding (the options binding) to finish DOM creation, what's why you use setTimeout. But ko provided a way to define sequence of the bindings, just look the source code of ko value binding, it's defined as 'after': ['options', 'foreach']
third, your select2 doesn't respond to outside change. For instance, if you have another UI to update $root.selectedItem (a normal select list), the change raised by that UI would not sync back to your select2.
The solution
Define select2 binding based on existing value binding (just found out it's not needed), and hook up all change events.
we don't need "select2-removed" event, it's all about "change" event.
select2 provided all drawing, we don't need update callback.
use a flag shouldIgnore to break the loop between value subscriber and select2 change event handler.
http://jsfiddle.net/huocp/8N3zX/6/
http://jsfiddle.net/huocp/8N3zX/9/
ko.bindingHandlers.valueSelect2 = {
'after': ['options'],
'init': function(element, valueAccessor, allBindings) {
// kind of extend value binding
// ko.bindingHandlers.value.init(element, valueAccessor, allBindings);
var options = allBindings.get('select2Options') || {};
$(element).select2(options);
var value = valueAccessor();
// init val
$(element).val(ko.unwrap(value)).trigger("change");
var changeListener;
if (ko.isObservable(value)) {
var shouldIgnore = false;
changeListener = value.subscribe(function(newVal) {
if (! shouldIgnore) {
shouldIgnore = true;
// this is just select2 syntax
$(element).val(newVal).trigger("change");
shouldIgnore = false;
}
});
// this demo only works on single select.
$(element).on("change", function(e) {
if (! shouldIgnore) {
shouldIgnore = true;
if (e.val == '') {
// select2 use empty string for unselected value
// it could cause problem when you really want '' as a valid option
value(undefined);
} else {
value(e.val);
}
shouldIgnore = false;
}
});
}
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
if (changeListener) changeListener.dispose();
$(element).select2("destory");
});
}
};
I have set up the following select for changing semesters on page. When the select detects a change, the changeSemesters function is fired, which runs an AJAX that replaces the current data on the page with data specific to the selected semester.
View
<select data-bind="options: semestersArr, optionsText: 'name', optionsValue: 'id', value: selectedSemester, event: { change: changeSemesters }"></select>
ViewModel
this.selectedSemester = ko.observable();
//runs on page load
var getSemesters = function() {
var self = this, current;
return $.get('api/semesters', function(data) {
self.semestersArr(ko.utils.arrayMap(data.semesters, function(semester) {
if (semester.current) current = semester.id;
return new Model.Semester(semester);
}));
self.selectedSemester(current);
});
};
var changeSemesters = function() {
// run ajax to get new data
};
The problem is that the change event in the select fires the changeSemester function when the page loads and sets the default value. Is there a way to avoid that without the use of a button?
Generally what you want to do in these scenarios is to use a manual subscription, so that you can react to the observable changing rather than the change event. Observables will only notify when their value actually changes.
So, you would do:
this.selectedSemester.subscribe(changeSemesters, this);
If selectedSemester is still changing as a result of getting bound, then you can initialize it to the default value.
this.selectedSemester = ko.observable(someDefaultValue);