Reputation: 6116
I have a Knockout.js web application where I have a select2 dropdown. I want to bind both the id and text values to a variable instead of just the id. Here's my data:
var cars = [{id: 1, name: 'Honda'}, {id: 2, name: 'Toyota'}, {id: 3, name: 'Dodge'}];
var selectedCar = ko.observable();
Here's my html:
<select data-bind="value: selectedCar, optionsCaption: 'Select', optionsText: 'name', options: cars"></select>
So now, whenever I select something in the dropdown my variable contains the entire object like so:
selectedCar = {id: 1, name: 'Honda'};
The only problem occurs when you load the page and want the dropdown to be set to a specific value. Even though before rendering the html the selectedCar
variable is set to {id: 1, name: 'Honda'}
when the page renders the dropdown is not set to anything, it just is set to the placeholder 'Select'.
What am I doing wrong?
Upvotes: 1
Views: 1674
Reputation: 3959
If you want to bind both the id and text value to a variable, you need to use the optionsValue
binding and bind the entire context to it ($data
);
<select data-bind="value: selectedCar,
optionsCaption: 'Select',
optionsText: 'name',
options: cars,
optionsValue: $data"></select>
Normally we would specify the id or whatever to get an initial value selected. So it would make sense like in your question to supply the entire object {id: 1, name: 'Honda'}
as the initial value to selectedCar
if wet set optionsValue: $data
and not $optionsValue: 'id'
.
(IMPORTANT) But turns out this doesn't work, because we're creating a new object and so Knockout's equality test will fail when it compares the objects of cars
with the object inside selectedCar
. The correct way to set the initial value is cars[0]
.
I'm sure this is a typo, but I'll specify anyway: when you create any variable that needs to be accessed by the HTML you need to bind it to this
, which would be a reference to the viewModel.
this.cars = [{id: 1, name: 'Honda'}, {id: 2, name: 'Toyota'}, {id: 3, name: 'Dodge'}];
this.selectedCar = ko.observable();
Let's test all this with a fiddle:
var viewModel = function(){
var self = this;
self.cars = [{id: 1, name: 'Honda'}, {id: 2, name: 'Toyota'}, {id: 3, name: 'Dodge'}];
self.selectedCar = ko.observable(self.cars[0]);
};
ko.applyBindings(new viewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<select data-bind="value: selectedCar, optionsCaption: 'Select', optionsText: 'name', optionsValue:$data, options: cars"></select>
<!-- to verify that we are getting the entire object -->
<p data-bind="text: ko.toJSON(selectedCar)"></p>
Upvotes: 1
Reputation: 134811
The value
of a select box will become the object that corresponds to the selected item or the property set in the optionsValue
parameter. So in the case of objects, the selected value you set must be the same instance that exists in the array. Having a different instance of an object happens to be structurally equivalent is not enough.
For situations like this, I find it easier to bind the value of the select box to a unique id for the object instead. Then you could map out that id to the actual instance that you want through a computed value.
function ViewModel(data) {
this.cars = data.cars;
this.selectedCarId = ko.observable(data.selectedCarId);
this.selectedCar = ko.computed(() => {
let selectedCarId = this.selectedCarId();
return this.cars.find(c => c.id === selectedCarId);
});
}
let model = {
cars: [
{ id: 1, name: 'Honda' },
{ id: 2, name: 'Toyota' },
{ id: 3, name: 'Dodge' }
],
selectedCarId: 2
};
ko.applyBindings(new ViewModel(model), document.getElementById('content'));
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.5/js/select2.min.js"></script>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.5/css/select2.min.css">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div id="content">
<select data-bind="value: selectedCarId,
optionsCaption: 'Select',
optionsText: 'name',
optionsValue: 'id',
options: cars">
</select>
<p>selectedCarId: <span data-bind="text: selectedCarId"></span></p>
<p>selectedCar: <span data-bind="text: ko.toJSON(selectedCar)"></span></p>
<pre data-bind="text: ko.toJSON($root, null, 2)"></pre>
</div>
If you'd rather not use a separate computed property, you would still need to pass in an index, but the value you set, must be obtained from the array.
function ViewModel(data) {
this.cars = data.cars;
this.selectedCar = ko.observable(
data.cars.find(c => c.id === data.selectedCarId)
);
}
Upvotes: 0