I'm having an HTML dropdown, where I use Knockout.js to bind the options. With the dropdown, you can select ISO country codes. In the (short) dropdown, I want to display the two-letter country code as text. The full names of the countries should only appear, when the user opens the dropdown. Something like:
+=======+===+
| DE | v |
+=======+===+
| Germany |
| England |
| France |
| Spain |
| USA |
+-----------+
Right now, my HTML code looks like this:
<select class="form-control w-25" data-bind="
value: customAddress.country,
options: countryList,
optionsText: 'name',
optionsValue: 'code',
optionsCaption: 'Country'
" required></select>
Obviously, the dropdown displays "Germany" right now, if you select it. I found some ideas to replace the display text of the dropdown using jQuery in the onBlur event. But I fear, that this will interfere with the data binding mechanism of knockout (all properties are observables).
How can I solve that? Do I need a custom binding?
You don't necessarily need a custom binding handler (and certainly don't need to resort to jQuery); this could be accomplished using the default binding handlers.
Store the select menu state (open/closed) in a variable;
Toggle this variable on the blur/focus events using the event binding. If it's a focus event, we assume the menu is open; if it's a blur event, we assume menu is closed.
Pass a function to optionsText that will return either the code or the country, depending on the value of said variable.
JS:
function ViewModel() {
var vm = this;
vm.countries = [{
code: 'DE',
country: 'Germany'
},
{
code: 'NL',
country: 'The Netherlands'
},
{
code: 'BE',
country: 'Belgium'
}
];
vm.countrySelectIsOpen = ko.observable(false);
vm.selectedCountry = ko.observable();
vm.getOptionsText = function(item) {
return item[vm.countrySelectIsOpen() ? 'country' : 'code'];
}
vm.toggleCountrySelectStatus = function(data, event) {
vm.countrySelectIsOpen(event.type === 'focus');
}
}
ko.applyBindings(new ViewModel)
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<select class="form-control" data-bind="
options: countries,
optionsText: getOptionsText,
optionsValue: 'code',
optionsCaption: 'Country',
value: selectedCountry,
event: {
blur: toggleCountrySelectStatus,
focus: toggleCountrySelectStatus
}
"></select>
Fiddle: https://jsfiddle.net/thebluenile/hf70kg84/
Related
Well, I have two problems that worry me a lot ... First, I don't know how to give a default value to the select box.
And I I'm not able to change the value of the select box via an event click ... I've created a fiddle example if someone could give me a hand it would be very appreciated!
HTML
<select id="FilterBox" data-bind="value: siteGetOne">
<option value="-2">City Wide</option>
<!-- ko foreach: sites -->
<option data-bind="text: name, value: $data"></option>
<!-- /ko -->
</select>
Selection Option Object : <span data-bind="text: siteGetOne"></span><br/>
Selection Option name : <span data-bind="text: siteGetOne().name"></span><br/>
Selection Option id : <span data-bind="text: siteGetOne().id"></span><br/>
Set Value to 1
Set Value to 2
Set Value to 3
JS
var viewModel = function() {
var self = this;
setValue = ko.observable();
self.sites = [
{ name: 'Site 1', id: 1},
{ name: 'Site 2', id: 2},
{ name: 'Site 3', id: 3}
];
self.siteGetOne = ko.observable(self.sites[2].id);
self.siteGetOne.subscribe(function (newValue) {
console.log(newValue);
}, self);
}
ko.applyBindings(new viewModel());
http://jsfiddle.net/xjYcu/276/
Edited Final version : http://jsfiddle.net/xjYcu/286/
couple things you may want to change.
here is the entire fiddle. http://jsfiddle.net/xjYcu/283/
the first one is you should use the options binding for your select.
<select id="FilterBox" data-bind=" options: sites,
optionsText: 'name',
value: siteGetOne,
optionsCaption: 'Choose...'">
</select>
also try changing your click bindings to something like this so you can pass in your parameter.
Set Value to 1
Set Value to 2
Set Value to 3
So i am using knockout and trying to get the selected item's id in my javascript on the change event. Here is my html
<div id="platforms" data-bind="with: platformsViewModel">
<p>
Selected Platform:
<select data-bind="options: platforms, optionsText: 'displayName', value: 'id', optionsCaption: 'Choose...', event: { change: loadMedias }" ></select>
</p>
</div>
my view model is as follows
my.viewModels.PlatformsViewModel = function () {
var self = this;
self.platforms = ko.observableArray();
self.message = ko.observable();
self.loadMedias = function (data, event) {
my.loadMedias(data.id);
}
}
What am i missing here?
It looks like this may be a simple fix, where you're possibly using the value binding where you should be using the optionsValue binding:
<select data-bind="options: platforms, optionsText: 'displayName', optionsValue: 'id', optionsCaption: 'Choose...', event: { change: loadMedias }" ></select>
<!-- ^ here -->
However, why not put the logic in the view model, rather than in your view:
my.viewModels.PlatformsViewModel = function () {
var self = this;
self.platforms = ko.observableArray();
self.message = ko.observable();
//new observable to hold selected platform
self.selectedPlatform = ko.observable();
//subscribe to changes in the observable value to trigger the loading
self.selectedPlatform.subscribe(function(newValue) {
my.loadMedias(newValue.id);
});
}
And an updated <select> that will bind the actual platform object selected, rather than just its ID:
<select data-bind="options: platforms, optionsText: 'displayName', value: selectedPlatform, optionsCaption: 'Choose...'" ></select>
Html:
<select name="ddlUsers" id="ddlUsers" class="form-control"
data-bind="options: ViewModel.CashierPage.AvailableCash, optionsText: 'OptionTextInfo', value: ViewModel.CashierPage.CashSelected, optionsCaption: 'Cassa...'"></select>
in js:
public CashSelected: KnockoutObservable();
...
self.CashSelected = ko.observable(null);
self.CashSelected.subscribe(function(valueNewValue){/*your code*/});
Code in controller:
$scope.infoOptions = [
{ name: 'Select Option', value: '0' },
{ name: 'Some Option', value: '1' }
];
HTML:
<select data-ng-model="nothing" data-ng-options="info.name for info in infoOptions ">
</select>
Angular puts that damn empty option at the top/selected by default. I've seen some answers to this question that suggest selecting a default option in the $scope for the form, but this select box is in a template in a dynamic form (ie. can be a number of select boxes). This is really only for demonstration purposes - is there anyway I can get rid of that empty option in a template?
I'm fairly new to knockout.js, however, I've been happily using it in my ASP.NET MVC 4 project, until I ran into this obstacle which has been bothering me for a while, can't seem to put my finger on it.
The scenario which I'm working on requires several combinations of location data (region, country, city), i.e. cascading dropdown lists, which isn't a problem to do when inputting fresh data, but I ran into problem(s) when trying to edit the saved data.
Data is in JSON format, with nested arrays, looks like this (shortened for illustration purposes):
var newData =
[
{
"ID":1,
"Name":"Australia and New Zealand",
"Countries":[
{
"ID":13,
"Name":"Australia",
"Cities":[
{
"ID":19,
"Name":"Brisbane"
},
{
"ID":28,
"Name":"Cairns"
},
...
I suspect I can't load the data (or more clearly, to bind it) properly since I'm having trouble accessing the Region sub-array (which contains Region's Countries) and the Countries sub-array (which contains Countries' Cities).
Then there's the matter of having prepopulated options, which works partially, the viewmodel loads the number of lines, but doesn't select anything.
Here's the VM:
var existingRows = [
{
"Region": 1,
"Country": 13,
"City": 19
},
{
"Region": 1,
"Country": 158,
"City": 3
}];
var Location = function (region, country, city) {
var self = this;
self.region = ko.observable(region);
self.country = ko.observable(country);
self.city = ko.observable(city);
// Whenever the region changes, reset the country selection
self.region.subscribe(function () {
self.country(undefined);
});
// Whenever the country changes, reset the city selection
self.country.subscribe(function () {
self.city(undefined);
});
};
var LocationViewModel = function (data) {
var self = this;
self.lines = ko.observableArray(ko.utils.arrayMap(data, function (row)
{
var rowRegion = ko.utils.arrayFirst(newData, function (region)
{
return region.ID == row.Region;
});
var rowCountry = ko.utils.arrayFirst(rowRegion.Countries, function (country) {
return country.ID == row.Country;
});
var rowCity = ko.utils.arrayFirst(rowCountry.Cities, function (city) {
return city.ID == row.City;
});
return new Location(rowRegion, rowCountry, rowCity);
}));
// Operations
self.addLine = function () {
self.lines.push(new Location())
};
self.removeLine = function (line) {
self.lines.remove(line)
};
};
var lvm = new LocationViewModel(existingRows);
$(function () {
ko.applyBindings(lvm);
});
HTML code:
<tbody data-bind="foreach: lines">
<tr>
<td><select data-bind="options: newData, optionsText: 'Name', optionsValue: 'ID', optionsCaption: 'Select a region...', attr: { name: 'SubRegionIndex' + '['+$index()+']' }, value: region"></select></td>
<td><select data-bind="options: Countries, optionsText: 'Name', optionsValue: 'ID', optionsCaption: 'Select a country...', attr: { name: 'CountryIndex' + '['+$index()+']' }, value: country"></select></td>
<td><select data-bind="options: Cities, optionsText: 'Name', optionsValue: 'ID', optionsCaption: 'Select a city...', attr: { name: 'CityIndex' + '['+$index()+']' }, value: city"></select></td>
<td><a href='#' data-bind='click: $parent.removeLine'>Remove</a></td>
</tr>
</tbody>
I tried to modify the Cart editor example from the knockout.js website with prepopulated data, but haven't really made much progress, I seem to be missing something. Didn't really find anything with nested arrays so I'm stuck here...
I've put up the full code on JSFiddle here:
http://jsfiddle.net/fgXA2/1/
Any help would be appreciated.
The problem is the way in which you are binding to the selected item in your select lists:
<select data-bind="
options: newData,
optionsText: 'Name',
optionsValue: 'ID',
value: region">
</select>
Here you are binding the ID property from your JSON data to the region property on your view model.
This means that when you bind your second select list:
<td data-bind="with: region">
<select data-bind="
options: Countries,
optionsText: 'Name',
optionsValue: 'ID',
value: $parent.country">
</select>
</td>
You attempt to bind to region.Countries. However, region simply contains the selected region ID. In this case the console is your friend:
Uncaught Error: Unable to parse bindings. Message: ReferenceError:
Countries is not defined;
The same problem is true of your third select list for Cities since you are now attempting to bind to country.Cities where country is also just the ID.
There are two options available here. The first is to remove the optionsValue parameters, thus binding the actual JSON objects to your view model properties. That and a binding error on your Cities select box (you were binding to CityName instead of Name) were the only problems:
http://jsfiddle.net/benfosterdev/wHtRZ/
As you can see from the example I've used the ko.toJSON utility to output your view model's object graph. This can be very useful in resolving problems (in your case you would have seen that the region property was just an number).
The downside of the above approach is that you end up storing a copy of all of the countries, and their cities for the selected country in your view model.
A better solution if dealing with large data sets would be to only store the selected identifier (which I believe you were attempting to do originally) and then define computed properties that filter your single data set for the required values.
An example of this can be seen at http://jsfiddle.net/benfosterdev/Bbbt3, using the following computed properties:
var getById = function (items, id) {
return ko.utils.arrayFirst(items, function (item) {
return item.ID === id;
});
};
this.countries = ko.computed(function () {
var region = getById(this.selectedRegion.regions, this.selectedRegion());
return region ? ko.utils.arrayMap(region.Countries, function (item) {
return {
ID: item.ID,
Name: item.Name
};
}) : [];
}, this);
this.cities = ko.computed(function () {
var region = getById(this.selectedRegion.regions, this.selectedRegion());
if (region) {
var country = getById(region.Countries, this.selectedCountry());
if (country) {
return country.Cities;
}
}
}, this);
You can see from the rendered object graph that only the currently selected countries and cities are copied to the view model.
I have a fairly simple order creation form on a back office app I'm building. The scenario I cannot figure out is under the "Order lines" section of the form. I want it so that you click add row the row appears which contains a drop down containing all the products, then 2 text boxes for quantity and price. I want it so that when a product is selected, that products price is set as the default value in the Price text box, but the use is able to change it still.
So far I have everything in- you can add rows, you can select the part- the only piece i cannot figure out how to do correctly is to populate the default price. So a cut down version of my knockout viewModel looks like this;
viewModel = {
Parts : ko.observableArray(initialData.Parts),
Sale : ko.observable(initialData.Sale),
SaleLines : ko.observableArray(initialData.SaleLines),
addRow: function() {
this.SaleLines.push({ Id: "00000000-0000-0000-0000-000000000000", SalePrice : 0.00, Qty : 1, PartId: "" });
$("select").combobox({
selected: function (event, ui) {
$(ui.item.parentNode).change();
}
});
},
removeRow: function (r) {
this.SaleLines.remove(r);
}
}
The Sale lines are rendered out through a template like this;
<script type="text/html" id="saleLineTemplate">
<tr>
<td>
<select data-bind='options: viewModel.Parts, optionsText: "Description", optionsCaption: "Select...", optionsValue: "Id", value: PartId, uniqueName: true' class="mustPopulateDropdown"></select>
</td>
<td>
<input data-bind="value: Qty, uniqueName: true" class="required number"/>
</td>
<td>
<input data-bind="value: SalePrice, uniqueName: true" class="required number"/>
</td>
<td>
Delete
</td>
</tr>
</script>
The actual Parts collection comes from the backend MVC, and is passed as a List with PartDTO just having Id, Description and Price.
I just cannot think of the correct way to do this- i figured I maybe make the Id field of each SaleLine observable when I create it then somehow make it update the SalePrice on update, but just can't think how to implement it?
Thanks in advance for your help!
How about something like this: http://jsfiddle.net/rniemeyer/VLVuC/
Basically, make it so the "value" of the dropdown is a "SelectedPart" observable that is set to the corresponding Part object:
<select data-bind='options: viewModel.Parts, optionsText: "Description", optionsCaption: "Select...", value: SelectedPart, uniqueName: true' class="mustPopulateDropdown"></select>
Then, subscribe to the SelectedPart changing and set your SalePrice based on SelectedPart().Price.
addRow: function() {
var newRow = {
Id: "00000000-0000-0000-0000-000000000000",
Qty: ko.observable(1),
SalePrice: ko.observable(0),
SelectedPart: ko.observable()
};
newRow.SelectedPart.subscribe(function() {
this.SalePrice(this.SelectedPart() ? this.SelectedPart().Price : 0);
}, newRow);
Whenever, the dropdown changes on a row, then the SalePrice will be defaulted to the new selected product's price (unless they pick the "Select..." line).
Hope this helps.